├── docs ├── favicon.ico ├── favicon.png ├── logging.png └── products.png ├── .github ├── media │ ├── repo-logo.png │ ├── screenshot-01.png │ ├── screenshot-02.png │ └── screenshot-03.png ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── task_generic.md │ ├── cve_report.md │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── FUNDING.yml ├── no-response.yml ├── CONTRIBUTING.md ├── config.yml ├── workflows │ ├── dotnet-develop.yml │ └── dotnet-master.yml └── CODE_OF_CONDUCT.md ├── src ├── Atlassian.Downloader.Core │ ├── favicon.ico │ ├── favicon.png │ ├── Models │ │ ├── ResponseItem.cs │ │ ├── DownloaderSettings.cs │ │ ├── MarketplaceModels.cs │ │ └── SourceInformation.cs │ ├── Atlassian.Downloader.Core.csproj │ ├── README.md │ └── AtlassianClient.cs ├── Atlassian.Downloader.Console │ ├── favicon.ico │ ├── favicon.png │ ├── .editorconfig │ ├── Properties │ │ └── launchSettings.json │ ├── Models │ │ ├── DownloaderOptions.cs │ │ ├── DownloadAction.cs │ │ └── UserAgents.cs │ ├── appsettings.json │ ├── Program.cs │ ├── atlassian-downloader.csproj │ ├── Worker.cs │ └── BellsAndWhistles.cs ├── atlassian-downloader.sln └── build.ps1 ├── SECURITY.md ├── LICENSE.md ├── CHANGELOG.md ├── .gitignore └── README.md /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/docs/favicon.png -------------------------------------------------------------------------------- /docs/logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/docs/logging.png -------------------------------------------------------------------------------- /docs/products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/docs/products.png -------------------------------------------------------------------------------- /.github/media/repo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/.github/media/repo-logo.png -------------------------------------------------------------------------------- /.github/media/screenshot-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/.github/media/screenshot-01.png -------------------------------------------------------------------------------- /.github/media/screenshot-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/.github/media/screenshot-02.png -------------------------------------------------------------------------------- /.github/media/screenshot-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/.github/media/screenshot-03.png -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/src/Atlassian.Downloader.Core/favicon.ico -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/src/Atlassian.Downloader.Core/favicon.png -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/src/Atlassian.Downloader.Console/favicon.ico -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicMorg/atlassian-downloader/HEAD/src/Atlassian.Downloader.Console/favicon.png -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS1591: Missing XML comment for publicly visible type or member 4 | dotnet_diagnostic.CS1591.severity = none 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/src" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "atlassian-downloader": { 4 | "commandName": "Project", 5 | "commandLineArgs": " --output-dir \"S:\\Vendors\\Atlassian\"" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task_generic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generic task 3 | about: Create a generic task 4 | title: '' 5 | labels: 'task, Regular Priority' 6 | assignees: 'stamepicmorg' 7 | 8 | --- 9 | 10 | **Describe** 11 | A clear and concise description of what the bug is. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain your problem. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Docker Engine Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | Docker >=19.x | :white_check_mark: | 8 | | Linux x86_64 Images | :white_check_mark: | 9 | 10 | ## Reporting a Vulnerability 11 | 1. Open `Issues` tab [here](https://github.com/EpicMorg/docker-scripts/issues). 12 | 2. Select `CVE Report`. 13 | 3. Publish `CVE Report`. 14 | 4. Thank you :heart: 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | _Describe the problem or feature in addition to a link to the issues._ 3 | 4 | ## Approach 5 | _How does this change address the problem?_ 6 | 7 | #### Open Questions and Pre-Merge TODOs 8 | - [ ] Use github checklists. When solved, check the box and explain the answer. 9 | 10 | ## Learning 11 | _Describe the research stage_ 12 | 13 | _Links to blog posts, patterns, libraries or addons used to solve this problem_ 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/cve_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: CVE report 3 | about: Create a report of some CVE 4 | title: '[CVE] ' 5 | labels: 'CVE, High Priority' 6 | assignees: 'stamepicmorg' 7 | 8 | --- 9 | **CVE number or URL** 10 | 11 | **Describe** 12 | A clear and concise description of what the CVE is. 13 | 14 | **Screenshots** 15 | If applicable, add screenshots to help explain your problem. 16 | 17 | **Additional context** 18 | Add any other context about the problem here. 19 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/Models/DownloaderOptions.cs: -------------------------------------------------------------------------------- 1 | // Atlassian.Downloader.Console/Models/DownloaderOptions.cs 2 | using System; 3 | 4 | namespace EpicMorg.Atlassian.Downloader.Models; 5 | 6 | public record DownloaderOptions( 7 | string OutputDir, 8 | Uri[]? CustomFeed, 9 | DownloadAction Action, 10 | bool About, 11 | string? ProductVersion, 12 | bool SkipFileCheck, 13 | string UserAgent, 14 | int MaxRetries, 15 | int DelayBetweenRetries, 16 | string? PluginId, 17 | bool RandomDelay, 18 | int MinDelay, 19 | int MaxDelay 20 | ); -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/Models/ResponseItem.cs: -------------------------------------------------------------------------------- 1 | namespace EpicMorg.Atlassian.Downloader; 2 | 3 | using System; 4 | 5 | public partial class ResponseItem 6 | { 7 | public string? Description { get; set; } 8 | public string? Edition { get; set; } 9 | public Uri? ZipUrl { get; set; } 10 | public object? TarUrl { get; set; } 11 | public string? Md5 { get; set; } 12 | public string? Size { get; set; } 13 | public string? Released { get; set; } 14 | public string? Type { get; set; } 15 | public string? Platform { get; set; } 16 | public required string Version { get; set; } 17 | public Uri? ReleaseNotes { get; set; } 18 | public Uri? UpgradeNotes { get; set; } 19 | } -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/Models/DownloadAction.cs: -------------------------------------------------------------------------------- 1 | namespace EpicMorg.Atlassian.Downloader; 2 | 3 | public enum DownloadAction 4 | { 5 | /// 6 | /// Download application files 7 | /// 8 | Download, 9 | /// 10 | /// Print download URLs and exit 11 | /// 12 | ListURLs, 13 | /// 14 | /// Print available application versions and exit 15 | /// 16 | ListVersions, 17 | /// 18 | /// Print feed JSONs to stdout and exit 19 | /// 20 | ShowRawJson, 21 | /// 22 | /// Download plugin files from Atlassian Marketplace 23 | /// 24 | Plugin, 25 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug, Regular Priority' 6 | assignees: 'stamepicmorg' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | patreon: kasthack 3 | custom: https://www.patreon.com/epicmorg 4 | ko_fi: epicmorg 5 | 6 | 7 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 8 | #open_collective: # Replace with a single Open Collective username 9 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 10 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 11 | #liberapay: # Replace with a single Liberapay username 12 | #issuehunt: # Replace with a single IssueHunt username 13 | #otechie: # Replace with a single Otechie username 14 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 7 5 | # Label requiring a response 6 | responseRequiredLabel: more-information-needed 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please reach out if you have or find the answers we need so 13 | that we can investigate further. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'Feature request, help wanted' 6 | assignees: 'stamepicmorg' 7 | milestone: 'Due 2021 🙏' 8 | 9 | --- 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": "Information", 4 | "WriteTo": [ 5 | { 6 | "Name": "Console", 7 | "Args": { 8 | "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Literate, Serilog.Sinks.Console" 9 | } 10 | } 11 | , 12 | { 13 | "Name": "Logger", 14 | "Args": { 15 | "configureLogger": { 16 | "WriteTo": [ 17 | { 18 | "Name": "RollingFile", 19 | "Args": { 20 | "pathFormat": "log.{Date}.log", 21 | "retainedFileCountLimit": 5 22 | } 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/Models/DownloaderSettings.cs: -------------------------------------------------------------------------------- 1 | // Atlassian.Downloader.Core/Models/DownloaderSettings.cs 2 | namespace EpicMorg.Atlassian.Downloader.Core.Models; 3 | 4 | public class DownloaderSettings 5 | { 6 | public required string OutputDir { get; set; } 7 | public Uri[]? CustomFeed { get; set; } 8 | public string? ProductVersion { get; set; } 9 | public bool SkipFileCheck { get; set; } 10 | public string UserAgent { get; set; } = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0"; 11 | public int MaxRetries { get; set; } = 5; 12 | public int DelayBetweenRetries { get; set; } = 2500; 13 | public bool RandomizeDelay { get; set; } = false; 14 | public int MinDelay { get; set; } = 300; 15 | public int MaxDelay { get; set; } = 10000; 16 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 2 | 3 | # Comment to be posted to on first time issues 4 | newIssueWelcomeComment: > 5 | :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. 6 | 7 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 8 | 9 | # Comment to be posted to on PRs from first time contributors in your repository 10 | newPRWelcomeComment: > 11 | Congratulations on opening your first Pull Request, this is a momentous day for you and us! :sparkles: 12 | 13 | 14 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 15 | 16 | # Comment to be posted to on pull requests merged by a first time user 17 | firstPRMergeComment: > 18 | Hooray! Your first Pull Request was merged, here's to many more :rocket: 19 | 20 | # It is recommend to include as many gifs and emojis as possible 21 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-develop.yml: -------------------------------------------------------------------------------- 1 | name: develop 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | tags: 8 | - '*' 9 | push: 10 | branches: 11 | - 'develop' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 8 23 | dotnet-quality: 'preview' 24 | 25 | - name: Restore 26 | env: 27 | DOTNET_CLI_TELEMETRY_OPTOUT: true 28 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 29 | run: | 30 | cd src 31 | dotnet restore 32 | 33 | - name: Build 34 | env: 35 | DOTNET_CLI_TELEMETRY_OPTOUT: true 36 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 37 | run: | 38 | cd src 39 | dotnet build --no-restore 40 | 41 | - name: Test 42 | env: 43 | DOTNET_CLI_TELEMETRY_OPTOUT: true 44 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 45 | run: | 46 | cd src 47 | dotnet test --no-build --verbosity normal 48 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - '*' 9 | schedule: 10 | - cron: '02 00 * * 6' # At 02:00, only on Saturday 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-dotnet@v3 20 | with: 21 | dotnet-version: 8 22 | dotnet-quality: 'preview' 23 | 24 | - name: Restore 25 | env: 26 | DOTNET_CLI_TELEMETRY_OPTOUT: true 27 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 28 | run: | 29 | cd src 30 | dotnet restore 31 | 32 | - name: Build 33 | env: 34 | DOTNET_CLI_TELEMETRY_OPTOUT: true 35 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 36 | run: | 37 | cd src 38 | dotnet build --no-restore 39 | 40 | - name: Test 41 | env: 42 | DOTNET_CLI_TELEMETRY_OPTOUT: true 43 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 44 | run: | 45 | cd src 46 | dotnet test --no-build --verbosity normal 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 EpicMorg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/atlassian-downloader.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.14.36518.9 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{119D41DA-BD17-42D2-8AC7-806C3B68223D}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | EndProjectSection 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Downloader.Core", "Atlassian.Downloader.Core\Atlassian.Downloader.Core.csproj", "{B83B9684-E5BF-CF27-8E53-A14CA330619B}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "atlassian-downloader", "Atlassian.Downloader.Console\atlassian-downloader.csproj", "{31C49AE0-52A2-3E49-F479-4E87A6566D5C}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {B83B9684-E5BF-CF27-8E53-A14CA330619B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {B83B9684-E5BF-CF27-8E53-A14CA330619B}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {B83B9684-E5BF-CF27-8E53-A14CA330619B}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {B83B9684-E5BF-CF27-8E53-A14CA330619B}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {31C49AE0-52A2-3E49-F479-4E87A6566D5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {31C49AE0-52A2-3E49-F479-4E87A6566D5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {31C49AE0-52A2-3E49-F479-4E87A6566D5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {31C49AE0-52A2-3E49-F479-4E87A6566D5C}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {6DE5A36D-883D-4DA1-9962-38FDD1EAD190} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/Program.cs: -------------------------------------------------------------------------------- 1 | // Atlassian.Downloader.Console/Program.cs 2 | using EpicMorg.Atlassian.Downloader.ConsoleApp; 3 | using EpicMorg.Atlassian.Downloader.Core; 4 | using EpicMorg.Atlassian.Downloader.Models; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Serilog; 9 | using System; 10 | using System.Threading.Tasks; 11 | 12 | namespace EpicMorg.Atlassian.Downloader; 13 | 14 | public class Program 15 | { 16 | static async Task Main( 17 | DownloadAction action = DownloadAction.Download, 18 | string? outputDir = null, 19 | string? pluginId = null, 20 | string? productVersion = null, 21 | bool skipFileCheck = false, 22 | string userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0", 23 | int maxRetries = 5, 24 | int delayBetweenRetries = 2500, 25 | Uri[]? customFeed = null, 26 | bool about = false, 27 | bool randomUserAgent = false, 28 | bool randomDelay = false, 29 | int minDelay = 300, 30 | int maxDelay = 10000 31 | ) 32 | { 33 | // Manually create the options object from the parsed parameters 34 | var options = new DownloaderOptions( 35 | outputDir ?? Environment.CurrentDirectory, 36 | customFeed, 37 | action, 38 | about, 39 | productVersion, 40 | skipFileCheck, 41 | userAgent, 42 | maxRetries, 43 | delayBetweenRetries, 44 | pluginId, 45 | randomDelay, 46 | minDelay, 47 | maxDelay 48 | ); 49 | 50 | await Host.CreateDefaultBuilder() 51 | .ConfigureAppConfiguration((hostingContext, config) => 52 | { 53 | config.SetBasePath(AppContext.BaseDirectory); 54 | }) 55 | .ConfigureServices((hostContext, services) => 56 | { 57 | services.AddSingleton(options); 58 | services.AddHttpClient(); 59 | services.AddHostedService(); 60 | }) 61 | .UseSerilog((context, services, configuration) => configuration 62 | .ReadFrom.Configuration(context.Configuration)) 63 | .RunConsoleAsync(); 64 | } 65 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Atlassian Downloader - Changelog 2 | 3 | ## 2.x 4 | 5 | * `2.0.0.8` - technical update: 6 | * updated libs 7 | * `2.0.0.7` - technical update: 8 | * added delays 9 | * added randomazin useragents 10 | * switched to `dotnet10` 11 | * `2.0.0.6` - technical update: 12 | * backported shim-fix for choco runs 13 | * updated libs 14 | * `2.0.0.5` - technical update: 15 | * Splitted core logic to standalone library, 16 | * reworked build scripts, 17 | * added code signing, 18 | * cleanup code. 19 | * `2.0.0.4` - update: 20 | * Added support for downloading marketplace plugins 21 | * updated httpClient code 22 | * Updated dependencies. 23 | * `2.0.0.3` - minor update: 24 | * Updated dependencies. 25 | * `dotnet9` 26 | * updated to new JSON format from atlassian 27 | * `2.0.0.2` - minor update: 28 | * Added `maxRetries (default: 5)` and `delayBetweenRetries (default: 2500, milliseconds)` args, to redownload file if connection will be reset. 29 | * Updated dependencies. 30 | * `2.0.0.1` - minor update: 31 | * Fix default output dir, enable nullables, fix compiler warnings #43 32 | * Remove redundant parameters from publish profiles #42 33 | * `2.0.0.0` - migrated to `dotnet8` and updated libs. 34 | * code optimized by [@kasthack](https://github.com/kasthack). 35 | * reworked build scripts via `cli` and `vs`. 36 | * added new dists - `osx-arm64`, `linux-bionic-x64`. 37 | * added support of custom useragent via flag 38 | * added suppor of skipping existing files via flag 39 | ## 1.x 40 | * `1.1.0.0` - added automatic compare of local and remote file sizes. If they differ - the file will be re-downloaded. 41 | * `1.0.1.1` - minor update: added `UserAgent` to HTTP headers and added mirrors of json files. 42 | * `1.0.1.0` - added support of `Atlassian Bitbucket (Mesh)` product, updated deps, fixed `Chocolatey` support and start logic. 43 | * `1.0.0.9` - updated deps. 44 | * `1.0.0.8` - switched to `dontet6.0`, updated deps. 45 | * `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. 46 | * `1.0.0.6` - added support of `clover`. fixed broken json parsing. added new `logger`. 47 | * `1.0.0.5` - added support for `EAP` releases. 48 | * `1.0.0.4` - bump version. rewrited build scripts. added support of `arm` and `arm64`. 49 | * `1.0.0.3` - some cosmetics improvements. 50 | * `1.0.0.2` - some cosmetics improvements. 51 | * `1.0.0.1` - some improvements. added support of all available products. 52 | * `1.0.0.0` - test script. internal use. not published. 53 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/atlassian-downloader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | false 8 | favicon.ico 9 | Atlassian Downloader 10 | 2.0.0.8 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Always 32 | 33 | 34 | 35 | 36 | 37 | 3BAA227AD0DBA8DB55D0EFA14B74AA56B689601D 38 | http://timestamp.digicert.com 39 | $(OutputPath)$(AssemblyName).exe 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 3BAA227AD0DBA8DB55D0EFA14B74AA56B689601D 50 | http://timestamp.digicert.com 51 | $(PublishDir)$(AssemblyName).exe 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/Atlassian.Downloader.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0;net9.0;net8.0 5 | enable 6 | enable 7 | true 8 | 9 | EpicMorg.Atlassian.Downloader 10 | 1.0.0.3 11 | EpicMorg, kasthack, stam, EpicMorgDev 12 | Core library for downloading Atlassian products and plugins. 13 | https://github.com/EpicMorg/atlassian-downloader 14 | favicon.ico 15 | False 16 | True 17 | EpicMorg.Atlassian.Downloader 18 | EpicMorg 19 | README.md 20 | https://github.com/EpicMorg/atlassian-downloader 21 | git 22 | epicmorg, atlassian, tool, downloader 23 | Atlassian Downloader 24 | EpicMorg 25 | Atlassian Downloader 26 | favicon.png 27 | MIT 28 | True 29 | True 30 | snupkg 31 | 32 | 33 | 34 | full 35 | 36 | 37 | 38 | full 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | True 48 | \ 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | True 61 | \ 62 | 63 | 64 | 65 | 66 | 67 | 3baa227ad0dba8db55d0efa14b74aa56b689601d 68 | http://timestamp.digicert.com 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at developer@epicm.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/Models/MarketplaceModels.cs: -------------------------------------------------------------------------------- 1 | namespace EpicMorg.Atlassian.Downloader.Models; 2 | 3 | using System.Collections.Generic; 4 | using System.Text.Json.Serialization; 5 | 6 | // Represents the top-level info for a plugin from /rest/2/addons/{addonKey} 7 | public class MarketplacePlugin 8 | { 9 | [JsonPropertyName("key")] 10 | public string? Key { get; set; } 11 | 12 | [JsonPropertyName("name")] 13 | public string? Name { get; set; } 14 | } 15 | 16 | // Represents the paginated response from the /versions endpoint 17 | public class AddonVersionCollection 18 | { 19 | [JsonPropertyName("_links")] 20 | public CollectionLinks? Links { get; set; } 21 | 22 | [JsonPropertyName("_embedded")] 23 | public CollectionEmbedded? Embedded { get; set; } 24 | 25 | [JsonPropertyName("count")] 26 | public int Count { get; set; } 27 | } 28 | 29 | public class CollectionLinks 30 | { 31 | [JsonPropertyName("next")] 32 | public Link? Next { get; set; } 33 | } 34 | 35 | public class CollectionEmbedded 36 | { 37 | [JsonPropertyName("versions")] 38 | public AddonVersionSummary[]? Versions { get; set; } 39 | } 40 | 41 | // Represents a single version summary in the list from the /versions endpoint 42 | public class AddonVersionSummary 43 | { 44 | [JsonPropertyName("_links")] 45 | public VersionSummaryLinks? Links { get; set; } 46 | 47 | [JsonPropertyName("name")] 48 | public string? Name { get; set; } 49 | 50 | [JsonPropertyName("deployment")] 51 | public DeploymentInfo? Deployment { get; set; } 52 | } 53 | 54 | public class VersionSummaryLinks 55 | { 56 | [JsonPropertyName("self")] 57 | public Link? Self { get; set; } 58 | } 59 | 60 | public class DeploymentInfo 61 | { 62 | [JsonPropertyName("server")] 63 | public bool Server { get; set; } 64 | 65 | [JsonPropertyName("dataCenter")] 66 | public bool DataCenter { get; set; } 67 | } 68 | 69 | // Represents the detailed information for a single version 70 | public class AddonVersionDetail 71 | { 72 | [JsonPropertyName("_embedded")] 73 | public VersionDetailEmbedded? Embedded { get; set; } 74 | 75 | [JsonPropertyName("name")] 76 | public string? Name { get; set; } 77 | 78 | [JsonPropertyName("release")] 79 | public ReleaseInfo? Release { get; set; } 80 | 81 | [JsonPropertyName("compatibilities")] 82 | public CompatibilityInfo[]? Compatibilities { get; set; } 83 | 84 | [JsonPropertyName("text")] 85 | public TextInfo? Text { get; set; } 86 | } 87 | 88 | public class VersionDetailEmbedded 89 | { 90 | [JsonPropertyName("artifact")] 91 | public ArtifactInfo? Artifact { get; set; } 92 | } 93 | 94 | public class ArtifactInfo 95 | { 96 | [JsonPropertyName("_links")] 97 | public ArtifactLinks? Links { get; set; } 98 | } 99 | 100 | public class ArtifactLinks 101 | { 102 | [JsonPropertyName("binary")] 103 | public Link? Binary { get; set; } 104 | } 105 | 106 | public class ReleaseInfo 107 | { 108 | [JsonPropertyName("date")] 109 | public string? Date { get; set; } 110 | } 111 | 112 | public class TextInfo 113 | { 114 | [JsonPropertyName("releaseNotes")] 115 | public string? ReleaseNotes { get; set; } 116 | } 117 | 118 | // Models for parsing compatibility ranges 119 | public class CompatibilityInfo 120 | { 121 | [JsonPropertyName("application")] 122 | public string? Application { get; set; } 123 | 124 | [JsonPropertyName("hosting")] 125 | public HostingInfo? Hosting { get; set; } 126 | } 127 | 128 | public class HostingInfo 129 | { 130 | [JsonPropertyName("dataCenter")] 131 | public VersionRange? DataCenter { get; set; } 132 | 133 | [JsonPropertyName("server")] 134 | public VersionRange? Server { get; set; } 135 | } 136 | 137 | public class VersionRange 138 | { 139 | [JsonPropertyName("min")] 140 | public VersionDetails? Min { get; set; } 141 | 142 | [JsonPropertyName("max")] 143 | public VersionDetails? Max { get; set; } 144 | } 145 | 146 | public class VersionDetails 147 | { 148 | [JsonPropertyName("version")] 149 | public string? Version { get; set; } 150 | } 151 | 152 | // Common helper class 153 | public class Link 154 | { 155 | [JsonPropertyName("href")] 156 | public string? Href { get; set; } 157 | } -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/Worker.cs: -------------------------------------------------------------------------------- 1 | using atlassian_downloader; 2 | using EpicMorg.Atlassian.Downloader.Core; 3 | using EpicMorg.Atlassian.Downloader.Core.Models; 4 | using EpicMorg.Atlassian.Downloader.Models; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace EpicMorg.Atlassian.Downloader.ConsoleApp; 13 | 14 | public class Worker : IHostedService 15 | { 16 | private readonly ILogger _logger; 17 | private readonly IHostApplicationLifetime _appLifetime; 18 | private readonly AtlassianClient _atlassianClient; 19 | private readonly DownloaderOptions _options; 20 | 21 | public Worker( 22 | ILogger logger, 23 | IHostApplicationLifetime appLifetime, 24 | AtlassianClient atlassianClient, 25 | DownloaderOptions options) 26 | { 27 | _logger = logger; 28 | _appLifetime = appLifetime; 29 | _atlassianClient = atlassianClient; 30 | _options = options; 31 | } 32 | 33 | public async Task StartAsync(CancellationToken cancellationToken) 34 | { 35 | // First, always show the banner 36 | BellsAndWhistles.ShowVersionInfo(_logger); 37 | 38 | // --- THIS IS THE FIX --- 39 | // If the --about flag was used, our only job is to show the banner. 40 | // We stop the application and immediately exit the method. 41 | if (_options.About) 42 | { 43 | _appLifetime.StopApplication(); 44 | return; 45 | } 46 | try 47 | { 48 | var settings = new DownloaderSettings 49 | { 50 | OutputDir = _options.OutputDir, 51 | SkipFileCheck = _options.SkipFileCheck, 52 | UserAgent = _options.UserAgent, 53 | MaxRetries = _options.MaxRetries, 54 | DelayBetweenRetries = _options.DelayBetweenRetries, 55 | CustomFeed = _options.CustomFeed, 56 | ProductVersion = _options.ProductVersion, 57 | RandomizeDelay = _options.RandomDelay, 58 | MinDelay = _options.MinDelay, 59 | MaxDelay = _options.MaxDelay 60 | }; 61 | 62 | switch (_options.Action) 63 | { 64 | case DownloadAction.Plugin: 65 | if (string.IsNullOrWhiteSpace(_options.PluginId)) 66 | { 67 | _logger.LogError("Action 'Plugin' requires a --plugin-id argument."); 68 | } 69 | else 70 | { 71 | await _atlassianClient.DownloadPluginAsync(_options.PluginId, settings, cancellationToken); 72 | } 73 | break; 74 | 75 | case DownloadAction.Download: 76 | await _atlassianClient.DownloadProductsAsync(settings, cancellationToken); 77 | break; 78 | 79 | case DownloadAction.ListURLs: 80 | case DownloadAction.ListVersions: 81 | case DownloadAction.ShowRawJson: 82 | var feedUrls = _atlassianClient.GetProductFeedUrls(settings); 83 | foreach (var feedUrl in feedUrls) 84 | { 85 | var (json, versions) = await _atlassianClient.GetProductDataAsync(feedUrl, settings, cancellationToken); 86 | 87 | if (_options.Action == DownloadAction.ShowRawJson) 88 | { 89 | Console.Out.WriteLine(json); 90 | } 91 | else if (_options.Action == DownloadAction.ListVersions) 92 | { 93 | foreach (var v in versions.Keys) Console.Out.WriteLine(v); 94 | } 95 | else if (_options.Action == DownloadAction.ListURLs) 96 | { 97 | foreach (var url in versions.SelectMany(v => v.Value).Select(f => f.ZipUrl)) 98 | { 99 | if (url != null) Console.Out.WriteLine(url); 100 | } 101 | } 102 | } 103 | break; 104 | } 105 | } 106 | catch (Exception ex) 107 | { 108 | if (ex is not OperationCanceledException) 109 | { 110 | _logger.LogCritical(ex, "An unhandled exception occurred."); 111 | } 112 | } 113 | finally 114 | { 115 | _logger.LogInformation("Execution finished. Application will now shut down."); 116 | _appLifetime.StopApplication(); 117 | } 118 | } 119 | 120 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 121 | } -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/README.md: -------------------------------------------------------------------------------- 1 | # EpicMorg.Atlassian.Downloader 2 | 3 | [![NuGet Version](https://img.shields.io/nuget/v/EpicMorg.Atlassian.Downloader.svg)](https://www.nuget.org/packages/EpicMorg.Atlassian.Downloader/) 4 | 5 | 6 | `EpicMorg.Atlassian.Downloader` is a modern .NET library for programmatically downloading Atlassian Server/Data Center products and Marketplace plugins. It provides a simple, asynchronous API to handle interactions with Atlassian's download services, including paginated endpoints and artifact resolution. 7 | 8 | This library is the core engine for the [Atlassian Downloader](https://github.com/EpicMorg/atlassian-downloader) console utility. 9 | 10 | --- 11 | 12 | ## Installation 13 | 14 | Install the library via the NuGet package manager. 15 | 16 | ```powershell 17 | dotnet add package EpicMorg.Atlassian.Downloader 18 | ``` 19 | 20 | ## Usage 21 | The primary entry point for the library is the AtlassianClient class. It's designed to be used with dependency injection and IHttpClientFactory for proper HttpClient management. 22 | 23 | ### 1. Setup (Dependency Injection) 24 | In your Program.cs or startup configuration, register the AtlassianClient. 25 | 26 | ``` 27 | using EpicMorg.Atlassian.Downloader.Core; 28 | using Microsoft.Extensions.DependencyInjection; 29 | using Microsoft.Extensions.Hosting; 30 | 31 | var host = Host.CreateDefaultBuilder(args) 32 | .ConfigureServices((context, services) => 33 | { 34 | // Register the AtlassianClient and configure HttpClient for it 35 | services.AddHttpClient(); 36 | 37 | // Register your other application services 38 | // services.AddHostedService(); 39 | }) 40 | .Build(); 41 | ``` 42 | 43 | ### 2. Creating Settings 44 | All download operations require a DownloaderSettings object to configure their behavior. 45 | 46 | ``` 47 | using EpicMorg.Atlassian.Downloader.Core.Models; 48 | 49 | var settings = new DownloaderSettings 50 | { 51 | OutputDir = "C:\\atlassian-archive", 52 | SkipFileCheck = false, 53 | MaxRetries = 5, 54 | DelayBetweenRetries = 3000, 55 | UserAgent = "My-Awesome-App/1.0" 56 | }; 57 | ``` 58 | 59 | ### 3. API and Examples 60 | Once you have an instance of AtlassianClient (injected by your DI container), you can call its public methods. 61 | 62 | #### DownloadPluginAsync 63 | Downloads all available Server/Data Center versions of a specific Marketplace plugin. It automatically handles pagination, determines compatibility, creates an organized folder structure, and generates a readme.md for each version. 64 | 65 | #### Signature: 66 | 67 | `Task DownloadPluginAsync(string pluginId, DownloaderSettings settings, CancellationToken cancellationToken = default)` 68 | 69 | #### Example: 70 | 71 | ``` 72 | using EpicMorg.Atlassian.Downloader.Core; 73 | using Microsoft.Extensions.DependencyInjection; // For GetRequiredService 74 | 75 | // Get the client from your DI container 76 | var atlassianClient = host.Services.GetRequiredService(); 77 | 78 | try 79 | { 80 | string pluginId = "com.onresolve.jira.groovy.groovyrunner"; // Example: ScriptRunner for Jira 81 | await atlassianClient.DownloadPluginAsync(pluginId, settings); 82 | Console.WriteLine($"Successfully archived all versions of {pluginId}."); 83 | } 84 | catch (Exception ex) 85 | { 86 | Console.WriteLine($"An error occurred: {ex.Message}"); 87 | } 88 | ``` 89 | 90 | #### DownloadProductsAsync 91 | Downloads Atlassian products (like Jira, Confluence) from the official JSON data feeds. 92 | 93 | #### Signature: 94 | 95 | ``` 96 | Task DownloadProductsAsync(DownloaderSettings settings, CancellationToken cancellationToken = default) 97 | ``` 98 | 99 | Example: 100 | You can specify ProductVersion and CustomFeed in the settings object to customize the download. 101 | 102 | ``` 103 | // Download a specific version of Confluence 104 | var confluenceSettings = new DownloaderSettings 105 | { 106 | OutputDir = "C:\\atlassian-archive", 107 | ProductVersion = "8.5.3", 108 | CustomFeed = new Uri[] { new Uri("[https://my.atlassian.com/download/feeds/current/confluence.json](https://my.atlassian.com/download/feeds/current/confluence.json)") } 109 | }; 110 | 111 | try 112 | { 113 | await atlassianClient.DownloadProductsAsync(confluenceSettings); 114 | Console.WriteLine("Confluence 8.5.3 download process completed."); 115 | } 116 | catch (Exception ex) 117 | { 118 | Console.WriteLine($"An error occurred: {ex.Message}"); 119 | } 120 | ``` 121 | 122 | #### GetProductDataAsync & GetProductFeedUrls 123 | These are lower-level methods for more granular control, allowing you to fetch product data without immediately downloading the files. 124 | 125 | #### Signatures: 126 | 127 | ``` 128 | IReadOnlyList GetProductFeedUrls(DownloaderSettings settings); 129 | 130 | Task<(string json, IDictionary versions)> GetProductDataAsync(string feedUrl, DownloaderSettings settings, CancellationToken cancellationToken); 131 | ``` 132 | Example: Listing all available Jira versions without downloading. 133 | 134 | ``` 135 | var jiraSettings = new DownloaderSettings 136 | { 137 | OutputDir = "C:\\atlassian-archive", 138 | CustomFeed = new Uri[] { new Uri("[https://my.atlassian.com/download/feeds/current/jira-software.json](https://my.atlassian.com/download/feeds/current/jira-software.json)") } 139 | }; 140 | 141 | var jiraFeed = atlassianClient.GetProductFeedUrls(jiraSettings).First(); 142 | 143 | var (_, versions) = await atlassianClient.GetProductDataAsync(jiraFeed, jiraSettings, CancellationToken.None); 144 | 145 | Console.WriteLine($"--- Available Jira Software Versions ---"); 146 | foreach (var versionKey in versions.Keys) 147 | { 148 | Console.WriteLine(versionKey); 149 | } 150 | ``` -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/Models/SourceInformation.cs: -------------------------------------------------------------------------------- 1 | namespace EpicMorg.Atlassian.Downloader.Models; 2 | using System.Collections.Generic; 3 | 4 | internal static class SourceInformation 5 | { 6 | public static IReadOnlyList AtlassianSources { get; } = new[] { 7 | 8 | //official links 9 | "https://my.atlassian.com/download/feeds/archived/bamboo.json", 10 | "https://my.atlassian.com/download/feeds/archived/clover.json", 11 | "https://my.atlassian.com/download/feeds/archived/confluence.json", 12 | "https://my.atlassian.com/download/feeds/archived/crowd.json", 13 | "https://my.atlassian.com/download/feeds/archived/crucible.json", 14 | "https://my.atlassian.com/download/feeds/archived/fisheye.json", 15 | "https://my.atlassian.com/download/feeds/archived/jira-core.json", 16 | "https://my.atlassian.com/download/feeds/archived/jira-servicedesk.json", 17 | "https://my.atlassian.com/download/feeds/archived/jira-software.json", 18 | "https://my.atlassian.com/download/feeds/archived/jira.json", 19 | "https://my.atlassian.com/download/feeds/archived/stash.json", 20 | "https://my.atlassian.com/download/feeds/archived/mesh.json", 21 | 22 | //cdn mirror of official links 23 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/bamboo.json", 24 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/clover.json", 25 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/confluence.json", 26 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/crowd.json", 27 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/crucible.json", 28 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/fisheye.json", 29 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/jira-core.json", 30 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/jira-servicedesk.json", 31 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/jira-software.json", 32 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/jira.json", 33 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/stash.json", 34 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/mesh.json", 35 | 36 | //official links 37 | "https://my.atlassian.com/download/feeds/current/bamboo.json", 38 | "https://my.atlassian.com/download/feeds/current/clover.json", 39 | "https://my.atlassian.com/download/feeds/current/confluence.json", 40 | "https://my.atlassian.com/download/feeds/current/crowd.json", 41 | "https://my.atlassian.com/download/feeds/current/crucible.json", 42 | "https://my.atlassian.com/download/feeds/current/fisheye.json", 43 | "https://my.atlassian.com/download/feeds/current/jira-core.json", 44 | "https://my.atlassian.com/download/feeds/current/jira-servicedesk.json", 45 | "https://my.atlassian.com/download/feeds/current/jira-software.json", 46 | "https://my.atlassian.com/download/feeds/current/stash.json", 47 | "https://my.atlassian.com/download/feeds/current/mesh.json", 48 | 49 | //cdn mirror of official links 50 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/bamboo.json", 51 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/clover.json", 52 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/confluence.json", 53 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/crowd.json", 54 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/crucible.json", 55 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/fisheye.json", 56 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/jira-core.json", 57 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/jira-servicedesk.json", 58 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/jira-software.json", 59 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/stash.json", 60 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/mesh.json", 61 | 62 | //official links 63 | "https://my.atlassian.com/download/feeds/eap/bamboo.json", 64 | "https://my.atlassian.com/download/feeds/eap/confluence.json", 65 | "https://my.atlassian.com/download/feeds/eap/jira.json", 66 | "https://my.atlassian.com/download/feeds/eap/jira-servicedesk.json", 67 | "https://my.atlassian.com/download/feeds/eap/stash.json", 68 | //"https://my.atlassian.com/download/feeds/eap/mesh.json", //404 69 | 70 | //cdn mirror of official links 71 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/eap/bamboo.json", 72 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/eap/confluence.json", 73 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/eap/jira.json", 74 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/eap/jira-servicedesk.json", 75 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/eap/stash.json", 76 | //"https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/eap/mesh.json", //404 77 | 78 | //https://raw.githubusercontent.com/EpicMorg/atlassian-json/master/json-backups/archived/sourcetree.json //unstable link with r\l 79 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/sourcetree.json", 80 | 81 | //https://raw.githubusercontent.com/EpicMorg/atlassian-json/master/json-backups/current/sourcetree.json //unstable link with r\l 82 | "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/sourcetree.json" 83 | 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/BellsAndWhistles.cs: -------------------------------------------------------------------------------- 1 | namespace atlassian_downloader; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | 8 | using Microsoft.Extensions.Logging; 9 | using EpicMorg.Atlassian.Downloader.Core; 10 | 11 | internal class BellsAndWhistles 12 | { 13 | private static readonly string assemblyEnvironment = string.Format("[{1}, {0}]", RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(), RuntimeInformation.FrameworkDescription); 14 | 15 | private static readonly Assembly entryAssembly = Assembly.GetEntryAssembly()!; 16 | 17 | private static readonly string assemblyVersion = entryAssembly.GetName().Version!.ToString(); 18 | 19 | private static readonly string fileVersion = entryAssembly.GetCustomAttribute()!.Version; 20 | 21 | private static readonly string coreVersion = typeof(AtlassianClient).Assembly 22 | .GetCustomAttribute()?.Version ?? "Unknown"; 23 | 24 | private static readonly string assemblyName = entryAssembly.GetCustomAttribute()!.Product; 25 | const string assemblyBuildType = 26 | #if DEBUG 27 | "[Debug]" 28 | #else 29 | 30 | "[Release]" 31 | #endif 32 | ; 33 | 34 | private const ConsoleColor DEFAULT = ConsoleColor.Blue; 35 | 36 | public static void ShowVersionInfo(ILogger logger) 37 | { 38 | logger.LogInformation( 39 | "{assemblyName} {assemblyVersion} {assemblyEnvironment} {assemblyBuildType}", 40 | assemblyName, 41 | assemblyVersion, 42 | assemblyEnvironment, 43 | assemblyBuildType); 44 | Console.BackgroundColor = ConsoleColor.Black; 45 | WriteColorLine("%╔═╦═══════════════════════════════════════════════════════════════════════════════════════╦═╗"); 46 | WriteColorLine("%╠═╝ .''. %╚═%╣"); 47 | WriteColorLine("%║ .:cc;. %║"); 48 | WriteColorLine("%║ .;cccc;. %║"); 49 | WriteColorLine("%║ .;cccccc;. !╔══════════════════════════════════════════════╗ %║"); 50 | WriteColorLine($"%║ .:ccccccc;. !║ {assemblyName} !║ %║"); 51 | WriteColorLine("%║ 'ccccccccc;. !╠══════════════════════════════════════════════╣ %║"); 52 | WriteColorLine("%║ ,cccccccccc;. !║ &Code: @kasthack, @stam !║ %║"); 53 | WriteColorLine("%║ ,ccccccccccc;. !║ &GFX: @stam !║ %║"); 54 | WriteColorLine("%║ .... .:ccccccccccc;. !╠══════════════════════════════════════════════╣ %║"); 55 | WriteColorLine($"%║ .',,'..;cccccccccccc;. !║ &Version: {fileVersion} !║ %║"); 56 | WriteColorLine($"%║ .,,,,,'.';cccccccccccc;. !║ &Core: {coreVersion} !║ %║"); 57 | WriteColorLine("%║ .,;;;;;,'.':cccccccccccc;. !║ &GitHub: $EpicMorg/atlassian-downloader !║ %║"); 58 | WriteColorLine("%║ .;:;;;;;;,...:cccccccccccc;. !╠══════════════════════════════════════════════╣ %║"); 59 | WriteColorLine($"%║ .;:::::;;;;'. .;:ccccccccccc;. !║ &Runtime: {assemblyEnvironment} !║ %║"); 60 | WriteColorLine("%║ .:cc::::::::,. ..:ccccccccccc;. !╚══════════════════════════════════════════════╝ %║"); 61 | WriteColorLine("%║ .:cccccc:::::' .:ccccccccccc;. %║"); 62 | WriteColorLine("%║ .;:::::::::::,. .;:::::::::::,. %║"); 63 | WriteColorLine("%╠═╗ ............ ............ %╔═╣"); 64 | WriteColorLine("%╚═╩═══════════════════════════════════════════════════════════════════════════════════════╩═╝"); 65 | Console.ResetColor(); 66 | } 67 | public static void SetConsoleTitle() => Console.Title = $@"{assemblyName} {assemblyVersion} {assemblyEnvironment} - {assemblyBuildType}"; 68 | 69 | private static void WriteColorLine(string text, params object[] args) 70 | { 71 | Dictionary colors = new() 72 | { 73 | { '!', ConsoleColor.Red }, 74 | { '@', ConsoleColor.Green }, 75 | { '#', ConsoleColor.Blue }, 76 | { '$', ConsoleColor.Magenta }, 77 | { '&', ConsoleColor.Yellow }, 78 | { '%', ConsoleColor.Cyan } 79 | }; 80 | // TODO: word wrap, backslash escapes 81 | text = string.Format(text, args); 82 | var chunk = ""; 83 | var paren = false; 84 | for (var i = 0; i < text.Length; i++) 85 | { 86 | var c = text[i]; 87 | if (colors.ContainsKey(c) && StringNext(text, i) != ' ') 88 | { 89 | Console.Write(chunk); 90 | chunk = ""; 91 | if (StringNext(text, i) == '(') 92 | { 93 | i++; // skip past the paren 94 | paren = true; 95 | } 96 | 97 | Console.ForegroundColor = colors[c]; 98 | } 99 | else if (paren && c == ')') 100 | { 101 | paren = false; 102 | Console.ForegroundColor = DEFAULT; 103 | } 104 | else if (Console.ForegroundColor != DEFAULT) 105 | { 106 | Console.Write(c); 107 | if (c == ' ' && !paren) 108 | { 109 | Console.ForegroundColor = DEFAULT; 110 | } 111 | } 112 | else 113 | { 114 | chunk += c; 115 | } 116 | } 117 | 118 | Console.WriteLine(chunk); 119 | Console.ForegroundColor = DEFAULT; 120 | } 121 | 122 | private static char StringNext(string text, int index) => index < text.Length ? text[index + 1] : '\0'; 123 | 124 | } 125 | 126 | -------------------------------------------------------------------------------- /src/build.ps1: -------------------------------------------------------------------------------- 1 | # build.ps1 - Builds the Core library and packages the Console application. 2 | 3 | [CmdletBinding()] 4 | param ( 5 | # Add a switch to optionally create an additional Native AOT build for the console app 6 | [switch]$Aot 7 | ) 8 | 9 | # --- Configuration --- 10 | # All settings are in one place for easy updates. 11 | $CoreProjectFolder = "Atlassian.Downloader.Core" 12 | $CoreProjectFile = Join-Path $CoreProjectFolder "Atlassian.Downloader.Core.csproj" 13 | $CoreBinReleaseFolder = Join-Path $CoreProjectFolder "bin" $Configuration 14 | $CoreBinReleaseNugetFile = Join-Path $CoreBinReleaseFolder "*.nupkg" 15 | 16 | $ConsoleProjectName = "atlassian-downloader" 17 | $ConsoleProjectFolder = "Atlassian.Downloader.Console" 18 | $ConsoleProjectFile = Join-Path $ConsoleProjectFolder "$ConsoleProjectName.csproj" 19 | 20 | $Configuration = "Release" 21 | $Framework = "dotnet10.0" 22 | 23 | $sha1Thumbprint = "3BAA227AD0DBA8DB55D0EFA14B74AA56B689601D" 24 | $sha256Fingerprint = "678456D26F89DF46A2AE8522825C157A6F9B937E890BBB5E6D51D1A2CBBD8702" 25 | $TimeStampServer = "http://timestamp.digicert.com" 26 | 27 | $runtimes = @( 28 | "win-x64", "win-x86", "win-arm64", 29 | "osx-x64", "osx-arm64", 30 | "linux-x64", "linux-musl-x64", "linux-arm", "linux-arm64", "linux-bionic-x64" 31 | ) 32 | 33 | # --- Build Logic --- 34 | $env:DOTNET_CLI_TELEMETRY_OPTOUT = 'true' 35 | $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 'true' 36 | 37 | 38 | # ================================================== 39 | # STAGE 1: Build and Pack the Core Library 40 | # ================================================== 41 | Write-Host "==================================================" 42 | Write-Host "Processing Atlassian.Downloader.Core library..." -ForegroundColor Magenta 43 | 44 | # Step 1: Build the project. This will trigger signing the DLL and creating the .nupkg file. 45 | Write-Host "Building, signing DLL, and creating NuGet package..." 46 | Invoke-Expression "dotnet build $CoreProjectFile -c $Configuration" 47 | if ($LASTEXITCODE -ne 0) { 48 | Write-Host "ERROR: dotnet build failed for Core library. Aborting." -ForegroundColor Red 49 | return 50 | } 51 | 52 | # Step 2: Find the most recently created .nupkg file in the output directory. 53 | Write-Host "Searching for the created NuGet package..." 54 | $nupkgFile = Get-ChildItem -Path $CoreBinReleaseFolder -Recurse -Filter "*.nupkg" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 55 | 56 | if (-not $nupkgFile) { 57 | Write-Host "ERROR: Could not find any .nupkg file after the build." -ForegroundColor Red 58 | return 59 | } 60 | 61 | $nupkgPath = $nupkgFile.FullName 62 | Write-Host "Found package: $nupkgPath" -ForegroundColor Cyan 63 | 64 | # Step 3: Sign the package using the exact path we found. 65 | Write-Host "(SKIPPING) Signing NuGet package..." 66 | #dotnet nuget sign "$nupkgPath" --certificate-fingerprint $sha256Fingerprint --timestamper $TimeStampServer --overwrite 67 | if ($LASTEXITCODE -ne 0) { 68 | Write-Host "ERROR: dotnet nuget sign failed for Core library. Aborting." -ForegroundColor Red 69 | return 70 | } 71 | 72 | Write-Host "Core library processed successfully." -ForegroundColor Green 73 | 74 | # ================================================== 75 | # STAGE 2: Publish and Package the Console Application 76 | # ================================================== 77 | Write-Host "==================================================" 78 | Write-Host "Processing Atlassian.Downloader.Console application..." -ForegroundColor Magenta 79 | 80 | foreach ($rid in $runtimes) { 81 | Write-Host "--------------------------------------------------" 82 | Write-Host "Processing Runtime: $rid" -ForegroundColor Yellow 83 | 84 | # --- Standard Self-Contained Build --- 85 | Write-Host "Starting Standard Self-Contained build..." -ForegroundColor Cyan 86 | 87 | # MODIFIED: Paths are now absolute, constructed from the script's root location 88 | $publishDir = Join-Path $PSScriptRoot $ConsoleProjectFolder "bin\$Configuration\$Framework\$rid\publish" 89 | $archiveName = Join-Path $PSScriptRoot $ConsoleProjectFolder "bin\$ConsoleProjectName-$Framework-$rid.zip" 90 | 91 | Invoke-Expression "dotnet publish $ConsoleProjectFile -c $Configuration --runtime $rid -p:SelfContained=true -p:PublishTrimmed=false -p:PublishAot=false -p:PublishSingleFile=false -o $publishDir --force" 92 | if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: dotnet publish failed for $rid." -ForegroundColor Red; continue } 93 | 94 | Remove-Item (Join-Path $publishDir "*.pdb") -ErrorAction SilentlyContinue 95 | New-Item -Path (Join-Path $publishDir "createdump.exe.ignore") -ItemType File -Force | Out-Null 96 | 97 | Write-Host "Creating archive: $archiveName" 98 | Push-Location $publishDir # Temporarily enter the publish directory 99 | 7z a -tzip -mx5 -r -aoa $archiveName * | Out-Null # Archive its contents (*) 100 | Pop-Location # Go back 101 | if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: 7-Zip failed for $rid." -ForegroundColor Red } 102 | 103 | Write-Host "Successfully processed build for $rid." -ForegroundColor Green 104 | 105 | # --- Native AOT Build (Optional) --- 106 | if ($Aot) { 107 | Write-Host "--------------------------------------------------" 108 | Write-Host "Starting Native AOT build..." -ForegroundColor Cyan 109 | $publishDirAot = Join-Path $ConsoleProjectFolder "bin\$Configuration\$Framework\$rid\publish-aot" 110 | $archiveNameAot = Join-Path $ConsoleProjectFolder "bin\$ConsoleProjectName-$Framework-$rid-aot.zip" 111 | 112 | Invoke-Expression "dotnet publish $ConsoleProjectFile -c $Configuration --runtime $rid -p:SelfContained=false -p:PublishTrimmed=false -p:PublishAot=true -p:PublishSingleFile=false -o $publishDirAot --force" 113 | if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: dotnet publish (AOT) failed for $rid." -ForegroundColor Red; continue } 114 | 115 | Remove-Item (Join-Path $publishDirAot "*.pdb") -ErrorAction SilentlyContinue 116 | 117 | Push-Location $publishDirAot 118 | 7z a -tzip -mx5 -r -aoa $archiveNameAot * | Out-Null 119 | Pop-Location 120 | if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: 7-Zip (AOT) failed for $rid." -ForegroundColor Red } 121 | 122 | Write-Host "Successfully processed AOT build for $rid." -ForegroundColor Green 123 | } 124 | } 125 | 126 | Write-Host "==================================================" 127 | Write-Host "All builds are complete." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Activity](https://img.shields.io/github/commit-activity/m/EpicMorg/atlassian-downloader?label=commits&style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/commits) [![GitHub issues](https://img.shields.io/github/issues/EpicMorg/atlassian-downloader.svg?style=popout-square)](https://github.com/EpicMorg/atlassian-downloader/issues) [![GitHub forks](https://img.shields.io/github/forks/EpicMorg/atlassian-downloader.svg?style=popout-square)](https://github.com/EpicMorg/atlassian-downloader/network) [![GitHub stars](https://img.shields.io/github/stars/EpicMorg/atlassian-downloader.svg?style=popout-square)](https://github.com/EpicMorg/atlassian-downloader/stargazers) [![Size](https://img.shields.io/github/repo-size/EpicMorg/atlassian-downloader?label=size&style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/archive/master.zip) [![Release](https://img.shields.io/github/v/release/EpicMorg/atlassian-downloader?style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/releases) [![Downloads](https://img.shields.io/github/downloads/EpicMorg/atlassian-downloader/total.svg?style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/releases) [![GitHub license](https://img.shields.io/github/license/EpicMorg/atlassian-downloader.svg?style=popout-square)](LICENSE.md) [![Changelog](https://img.shields.io/badge/Changelog-yellow.svg?style=popout-square)](CHANGELOG.md) 2 | 3 | # Atlassian Downloader 4 | 5 | Console app written with `c#` and `dotnet9` for downloading all avalible products from `Atlassian`. Why not? 6 | 7 | ![Atlassian Downloader](https://rawcdn.githack.com/EpicMorg/atlassian-downloader/28d17af55fbd4944d75f70d6bcb702e409820f64/.github/media/screenshot-01.png) 8 | ![Atlassian Downloader](https://rawcdn.githack.com/EpicMorg/atlassian-downloader/28d17af55fbd4944d75f70d6bcb702e409820f64/.github/media/screenshot-03.png) 9 | 10 | # Supported OS: 11 | `win-x86`, `win-x64`, `win-arm64`, `linux-x86`, `linux-x64`, `linux-musl-x64`, `linux-arm`, `linux-arm64`, `linux-bionic-x64`, `osx-x64`, `osx-arm64` 12 | 13 | ------------------- 14 | 15 | # How to... 16 | 17 | ## ..develop 18 | 1. preinstall `dotnet10`. Download [here](https://dotnet.microsoft.com/download/dotnet/10.0). 19 | 2. preinstall `VS2022`. Download [here](https://visualstudio.microsoft.com/vs/). 20 | 3. `git clone` this repo. 21 | 4. `cd` to `/src`. 22 | 5. open `*.sln` file 23 | 6. ... 24 | 7. profit! 25 | 26 | ## ..build from scratch 27 | 1. `git clone` this repo. 28 | 2. `cd` to `/src`. 29 | 3. execute `build.bat(sh)` in `src` folder. 30 | 4. by default all data will be downloaded to `src/Atlassian` folder and subfolders. 31 | 32 | ## ..use binary versions 33 | 1. just download latest [![Downloads](https://img.shields.io/github/downloads/EpicMorg/atlassian-downloader/total.svg?style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/releases) [![Release](https://img.shields.io/github/v/release/EpicMorg/atlassian-downloader?style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/releases) 34 | 2. ... 35 | 3. profit! 36 | 37 | ## ..intall via Chocolatey 38 | | CLI | Version | Downloads 39 | | ------ | ------ | ------ 40 | | :computer: `choco install atlassian-downloader` | [![Version](https://img.shields.io/chocolatey/v/atlassian-downloader?label=version&style=for-the-badge)](https://chocolatey.org/packages/atlassian-downloader/) | [![Version](https://img.shields.io/chocolatey/dt/atlassian-downloader?style=for-the-badge)](https://chocolatey.org/packages/atlassian-downloader/) 41 | 42 | ------------------- 43 | 44 | # Usage and settings 45 | ## CLI args 46 | 47 | ![Atlassian Downloader](https://rawcdn.githack.com/EpicMorg/atlassian-downloader/28d17af55fbd4944d75f70d6bcb702e409820f64/.github/media/screenshot-02.png) 48 | 49 | ``` 50 | atlassian-downloader: 51 | Atlassian archive downloader. See https://github.com/EpicMorg/atlassian-downloader for more info 52 | 53 | Usage: 54 | atlassian-downloader [options] 55 | 56 | Options: 57 | --output-dir Override output directory to download 58 | --custom-feed Override URIs to import [] 59 | --action Action to perform [default: Download] 60 | --about Show credits banner [default: False] 61 | --product-version Override target version to download some product. Advice: Use 62 | it with "customFeed". [] 63 | --skip-file-check Skip compare of file sizes if a local file already exists. 64 | Existing file will be skipped to check and redownload. 65 | [default: False] 66 | --user-agent Set custom user agent via this feature flag. [default: 67 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) 68 | Gecko/20100101 Firefox/101.0] 69 | --version Show version information 70 | -?, -h, --help Show help and usage information 71 | ``` 72 | 73 | ## Example of usage: 74 | 75 | ### How to download it all at first time, or get update of local archive 76 | ``` 77 | PS> .\atlassian-downloader.exe --output-dir "P:\Atlassian" 78 | or 79 | bash# ./atlassian-downloader --output-dir "/mnt/nfs/atlassian" 80 | ``` 81 | If you already have some folders at output path - they will be ignored and not be downloaded again and skipped. Downloader will be download only new versions of files which not be present locally yet. 82 | 83 | ### Set only some url feed and dowload it: 84 | ``` 85 | PS> .\atlassian-downloader.exe --output-dir "P:\Atlassian" --custom-feed https://my.atlassian.com/download/feeds/current/bamboo.json 86 | or 87 | bash# ./atlassian-downloader --output-dir "/mnt/nfs/atlassian" --custom-feed https://my.atlassian.com/download/feeds/current/bamboo.json 88 | ``` 89 | 90 | ### cron or crontab example 91 | ``` 92 | 0 0 * 1 0 /opt/epicmorg/atlassian-downloader/atlassian-downloader --output-dir "/mnt/nfs/atlassian" 93 | ``` 94 | ### Show only urls from jsons 95 | ``` 96 | PS> .\atlassian-downloader.exe --action ListURLs 97 | or 98 | bash# ./atlassian-downloader --action ListURLs 99 | ``` 100 | 101 | ## Additional settings 102 | 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. 103 | 104 | ### Supported log levels 105 | | Level | Enum | Description 106 | |-------------|:-------------:|-------------| 107 | | `Critical` | `5` | Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention. 108 | | `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. 109 | | `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. 110 | | `Information` | `2` | Logs that track the general flow of the application. These logs should have long-term value. 111 | | `None` | `6` | Not used for writing log messages. Specifies that a logging category should not write any messages. 112 | | `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. 113 | | `Warning` | `3` | Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop. 114 | 115 | ### Supported console themes 116 | The following built-in themes are available, provided by `Serilog.Sinks.Console` package: 117 | 118 | * `ConsoleTheme.None` - no styling 119 | * `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** 120 | * `SystemConsoleTheme.Grayscale` - a theme using only shades of gray, white, and black 121 | * `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 122 | * `AnsiConsoleTheme.Grayscale` - an ANSI 256-color version of the "grayscale" theme 123 | * `AnsiConsoleTheme.Code` - an ANSI 256-color Visual Studio Code-inspired theme 124 | 125 | ------------------- 126 | 127 | # Supported products: 128 | 129 | | Product | Current | Archive | EAP | 130 | |-------------|:-------------:|:-------------:|:-------------:| 131 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Bamboo&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/bamboo) | :white_check_mark: | :white_check_mark: | :white_check_mark: | 132 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Bitbucket%20(Stash)&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/bitbucket) | :white_check_mark: | :white_check_mark: | :interrobang: | 133 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Bitbucket%20(Mesh)&color=bright%20green&style=for-the-badge)](https://confluence.atlassian.com/bitbucketserver/bitbucket-mesh-compatibility-matrix-1127254859.html) | :white_check_mark: | :white_check_mark: | :interrobang: | 134 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Clover&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/clover) | :white_check_mark: | :white_check_mark: | :x: | 135 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Confluence&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/confluence) | :white_check_mark: | :white_check_mark: | :x: | 136 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Crowd&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/crowd) | :white_check_mark: | :white_check_mark: | :x: | 137 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Crucible&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/crucible) | :white_check_mark: | :white_check_mark: | :x: | 138 | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=FishEye&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/fisheye) | :white_check_mark: | :white_check_mark: | :x: | 139 | | [![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: | 140 | | [![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: | 141 | | [![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: | 142 | | [![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: | 143 | 144 | * Archive of `Atlassian` jsons available [here](https://github.com/EpicMorg/atlassian-json). 145 | 146 | ------------------- 147 | 148 | ## Authors 149 | * [@kasthack](https://github.com/kasthack) - code 150 | * [@stam](https://github.com/stamepicmorg) - code, repo 151 | -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Core/AtlassianClient.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace EpicMorg.Atlassian.Downloader.Core; 3 | 4 | using EpicMorg.Atlassian.Downloader.Core.Models; 5 | using EpicMorg.Atlassian.Downloader.Models; 6 | using Microsoft.Extensions.Logging; 7 | using ReverseMarkdown; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Net.Http; 13 | using System.Text.Json; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | public class AtlassianClient 18 | { 19 | private static readonly JsonSerializerOptions jsonOptions = new() { PropertyNameCaseInsensitive = true }; 20 | private readonly ILogger _logger; 21 | private readonly HttpClient _client; 22 | 23 | public AtlassianClient(HttpClient client, ILogger logger) 24 | { 25 | _client = client; 26 | _logger = logger; 27 | } 28 | 29 | #region Public API Methods 30 | 31 | public async Task DownloadPluginAsync(string pluginId, DownloaderSettings settings, CancellationToken cancellationToken = default) 32 | { 33 | if (string.IsNullOrWhiteSpace(pluginId)) 34 | { 35 | throw new ArgumentNullException(nameof(pluginId)); 36 | } 37 | 38 | _logger.LogInformation("Starting plugin archival for {pluginId}", pluginId); 39 | _client.DefaultRequestHeaders.UserAgent.ParseAdd(settings.UserAgent); 40 | 41 | try 42 | { 43 | var allVersions = await GetAllPluginVersions(pluginId, cancellationToken); 44 | if (!allVersions.Any()) 45 | { 46 | _logger.LogWarning("No versions found for plugin {pluginId}", pluginId); 47 | return; 48 | } 49 | 50 | _logger.LogInformation("Found a total of {count} versions. Processing...", allVersions.Count); 51 | 52 | var pluginInfo = await GetPluginInfo(pluginId, cancellationToken); 53 | if (pluginInfo is null) 54 | { 55 | _logger.LogError("Could not retrieve basic info for plugin {pluginId}", pluginId); 56 | return; 57 | } 58 | 59 | await DownloadPluginVersions(pluginInfo, allVersions, settings, cancellationToken); 60 | } 61 | catch (OperationCanceledException) 62 | { 63 | _logger.LogWarning("Plugin download process was canceled by the user."); 64 | throw; // Re-throw to allow the host to shut down gracefully 65 | } 66 | catch (Exception ex) 67 | { 68 | _logger.LogError(ex, "An error occurred during the plugin download process for {pluginId}", pluginId); 69 | } 70 | } 71 | 72 | public async Task DownloadProductsAsync(DownloaderSettings settings, CancellationToken cancellationToken = default) 73 | { 74 | _client.DefaultRequestHeaders.UserAgent.ParseAdd(settings.UserAgent); 75 | var feedUrls = GetFeedUrls(settings.CustomFeed); 76 | _logger.LogInformation("Product download task started."); 77 | 78 | foreach (var feedUrl in feedUrls) 79 | { 80 | cancellationToken.ThrowIfCancellationRequested(); 81 | var (_, versions) = await GetJson(feedUrl, settings.ProductVersion, cancellationToken); 82 | await DownloadFilesFromFeed(feedUrl, versions, settings, cancellationToken); 83 | } 84 | } 85 | 86 | public async Task<(string json, IDictionary versions)> GetProductDataAsync(string feedUrl, DownloaderSettings settings, CancellationToken cancellationToken) 87 | { 88 | _client.DefaultRequestHeaders.UserAgent.ParseAdd(settings.UserAgent); 89 | return await GetJson(feedUrl, settings.ProductVersion, cancellationToken); 90 | } 91 | 92 | public IReadOnlyList GetProductFeedUrls(DownloaderSettings settings) => GetFeedUrls(settings.CustomFeed); 93 | 94 | #endregion 95 | 96 | 97 | #region Private Helpers - Plugins 98 | 99 | private async Task GetPluginInfo(string pluginId, CancellationToken cancellationToken) 100 | { 101 | var pluginInfoUrl = $"https://marketplace.atlassian.com/rest/2/addons/{pluginId}"; 102 | _logger.LogDebug("Getting plugin info from: {url}", pluginInfoUrl); 103 | var pluginInfoJson = await _client.GetStringAsync(pluginInfoUrl, cancellationToken).ConfigureAwait(false); 104 | return JsonSerializer.Deserialize(pluginInfoJson, jsonOptions); 105 | } 106 | 107 | private async Task> GetAllPluginVersions(string pluginId, CancellationToken cancellationToken) 108 | { 109 | var allVersions = new List(); 110 | var nextUrl = $"https://marketplace.atlassian.com/rest/2/addons/{pluginId}/versions"; 111 | 112 | do 113 | { 114 | cancellationToken.ThrowIfCancellationRequested(); 115 | _logger.LogDebug("Fetching versions from: {url}", nextUrl); 116 | 117 | try 118 | { 119 | var responseJson = await _client.GetStringAsync(nextUrl, cancellationToken).ConfigureAwait(false); 120 | var page = JsonSerializer.Deserialize(responseJson, jsonOptions); 121 | 122 | if (page?.Embedded?.Versions != null) 123 | { 124 | allVersions.AddRange(page.Embedded.Versions); 125 | } 126 | 127 | // Prepare URL for the next iteration 128 | nextUrl = page?.Links?.Next?.Href; 129 | if (nextUrl != null && !nextUrl.StartsWith("http")) 130 | { 131 | nextUrl = $"https://marketplace.atlassian.com{nextUrl}"; 132 | } 133 | } 134 | catch (HttpRequestException httpEx) when (httpEx.StatusCode == System.Net.HttpStatusCode.NotFound) 135 | { 136 | // This is an expected "error" if the pluginId is invalid. 137 | // Log a clear message and break the loop. 138 | _logger.LogError("Plugin with ID '{pluginId}' not found on Atlassian Marketplace (404).", pluginId); 139 | nextUrl = null; // This will stop the do-while loop 140 | } 141 | // All other exceptions (network errors, other HTTP errors) will bubble up and be handled 142 | // by the top-level try-catch, which is the correct behavior. 143 | 144 | } while (!string.IsNullOrWhiteSpace(nextUrl)); 145 | 146 | return allVersions; 147 | } 148 | 149 | private async Task DownloadPluginVersions(MarketplacePlugin plugin, IEnumerable versions, DownloaderSettings settings, CancellationToken cancellationToken) 150 | { 151 | foreach (var version in versions) 152 | { 153 | cancellationToken.ThrowIfCancellationRequested(); 154 | 155 | if (version.Deployment?.Server != true && version.Deployment?.DataCenter != true) 156 | { 157 | _logger.LogDebug("Skipping version {versionName} (not for Server/DC)", version.Name); 158 | continue; 159 | } 160 | 161 | var versionDetail = await GetDetailedVersionInfo(plugin, version, cancellationToken); 162 | if (versionDetail is null || !versionDetail.Value.CompatibleProducts.Any()) 163 | { 164 | continue; 165 | } 166 | 167 | _logger.LogInformation("Plugin {pluginKey} v{version} is compatible with: {products}", plugin.Key, version.Name, string.Join(", ", versionDetail.Value.CompatibleProducts)); 168 | 169 | foreach (var product in versionDetail.Value.CompatibleProducts) 170 | { 171 | cancellationToken.ThrowIfCancellationRequested(); 172 | 173 | var sanitizedPluginName = SanitizeFolderName(plugin.Name ?? plugin.Key ?? "unknown-plugin"); 174 | var versionDir = Path.Combine(settings.OutputDir, "plugins", product, sanitizedPluginName, SanitizeFolderName(version.Name ?? "unknown-version")); 175 | 176 | if (!Directory.Exists(versionDir)) Directory.CreateDirectory(versionDir); 177 | 178 | var readmePath = Path.Combine(versionDir, "readme.md"); 179 | await File.WriteAllTextAsync(readmePath, versionDetail.Value.ReadmeContent ?? "No release notes.", cancellationToken); 180 | 181 | if (string.IsNullOrWhiteSpace(versionDetail.Value.DownloadUrl)) 182 | { 183 | _logger.LogWarning("No download URL found for version {version}", version.Name); 184 | continue; 185 | } 186 | 187 | var fileName = await GetActualFileNameAsync(versionDetail.Value.DownloadUrl, plugin.Key, version.Name, cancellationToken); 188 | var outputFile = Path.Combine(versionDir, fileName); 189 | 190 | if (File.Exists(outputFile) && settings.SkipFileCheck) 191 | { 192 | _logger.LogInformation("File {outputFile} already exists and skip check is enabled. Skipping.", outputFile); 193 | continue; 194 | } 195 | 196 | try 197 | { 198 | if (settings.RandomizeDelay) 199 | { 200 | var delay = Random.Shared.Next(settings.MinDelay, settings.MaxDelay); 201 | _logger.LogDebug("Throttling: Waiting {delay}ms before downloading plugin file...", delay); 202 | await Task.Delay(delay, cancellationToken).ConfigureAwait(false); 203 | } 204 | await DownloadPluginFile(versionDetail.Value.DownloadUrl, outputFile, settings, cancellationToken); 205 | } 206 | catch (Exception ex) 207 | { 208 | if (ex is not OperationCanceledException) 209 | { 210 | _logger.LogError("Failed to download {pluginName} v{version} for {product}. Reason: {message}", plugin.Name, version.Name, product, ex.Message); 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | private async Task<(string? DownloadUrl, string[] CompatibleProducts, string? ReadmeContent)?> GetDetailedVersionInfo(MarketplacePlugin plugin, AddonVersionSummary version, CancellationToken cancellationToken) 218 | { 219 | if (string.IsNullOrWhiteSpace(version.Links?.Self?.Href)) return null; 220 | 221 | try 222 | { 223 | var selfUrl = version.Links.Self.Href.StartsWith("http") 224 | ? version.Links.Self.Href 225 | : $"https://marketplace.atlassian.com{version.Links.Self.Href}"; 226 | 227 | var detailJson = await _client.GetStringAsync(selfUrl, cancellationToken); 228 | var detailVersion = JsonSerializer.Deserialize(detailJson, jsonOptions); 229 | if (detailVersion is null) return null; 230 | 231 | var downloadUrl = detailVersion.Embedded?.Artifact?.Links?.Binary?.Href; 232 | if (downloadUrl != null && !downloadUrl.StartsWith("http")) 233 | { 234 | downloadUrl = $"https://marketplace.atlassian.com{downloadUrl}"; 235 | } 236 | 237 | var compatibleProducts = detailVersion.Compatibilities? 238 | .Where(c => c.Application != null && (c.Hosting?.DataCenter != null || c.Hosting?.Server != null)) 239 | .Select(c => c.Application!) 240 | .Distinct() 241 | .ToArray() ?? Array.Empty(); 242 | 243 | var compatibilityLines = detailVersion.Compatibilities? 244 | .Where(c => c.Application != null && (c.Hosting?.DataCenter != null || c.Hosting?.Server != null)) 245 | .Select(c => 246 | { 247 | var range = c.Hosting?.DataCenter ?? c.Hosting?.Server; 248 | var min = range?.Min?.Version; 249 | var max = range?.Max?.Version; 250 | return $"* **{c.Application?.ToUpperInvariant()}**: {min} - {max}"; 251 | }) ?? Enumerable.Empty(); 252 | 253 | var converter = new Converter(); 254 | var htmlNotes = detailVersion.Text?.ReleaseNotes ?? "No release notes provided for this version."; 255 | var markdownNotes = converter.Convert(htmlNotes); 256 | 257 | var readmeContent = $""" 258 | # Release Notes for {plugin.Name} v{detailVersion.Name} 259 | 260 | ## Key Information 261 | * **Plugin Key**: `{plugin.Key}` 262 | * **Version**: `{detailVersion.Name}` 263 | * **Release Date**: `{detailVersion.Release?.Date ?? "N/A"}` 264 | 265 | ## Compatibility 266 | {string.Join("\n", compatibilityLines)} 267 | 268 | ## Release Notes 269 | {markdownNotes} 270 | """; 271 | 272 | return (downloadUrl, compatibleProducts, readmeContent); 273 | } 274 | catch (HttpRequestException httpEx) when (httpEx.StatusCode == System.Net.HttpStatusCode.NotFound || httpEx.StatusCode == System.Net.HttpStatusCode.BadRequest) 275 | { 276 | _logger.LogWarning("Could not get details for version {versionName} (HTTP {statusCode}). Skipping.", version.Name, httpEx.StatusCode); 277 | return null; 278 | } 279 | catch (OperationCanceledException) 280 | { 281 | throw; 282 | } 283 | catch (Exception ex) 284 | { 285 | _logger.LogError(ex, "Error getting detailed info for version {version}", version.Name); 286 | return null; 287 | } 288 | } 289 | 290 | private static string SanitizeFolderName(string name) 291 | { 292 | foreach (char c in Path.GetInvalidFileNameChars()) 293 | { 294 | name = name.Replace(c, '_'); 295 | } 296 | return name.Trim(); 297 | } 298 | 299 | private async Task GetActualFileNameAsync(string downloadUrl, string? pluginKey, string? version, CancellationToken cancellationToken) 300 | { 301 | try 302 | { 303 | using var headRequest = new HttpRequestMessage(HttpMethod.Head, downloadUrl); 304 | var headResponse = await _client.SendAsync(headRequest, cancellationToken); 305 | headResponse.EnsureSuccessStatusCode(); 306 | 307 | if (headResponse.Content.Headers.ContentDisposition?.FileName != null) 308 | { 309 | var fileName = headResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'); 310 | if (!string.IsNullOrWhiteSpace(fileName)) 311 | { 312 | _logger.LogDebug("Resolved filename from Content-Disposition header: {fileName}", fileName); 313 | return SanitizeFolderName(fileName); 314 | } 315 | } 316 | } 317 | catch (Exception ex) 318 | { 319 | _logger.LogDebug(ex, "HEAD request for filename failed, falling back to URL parsing."); 320 | } 321 | 322 | _logger.LogDebug("Could not resolve filename from headers, parsing URL instead."); 323 | return GetFileNameFromUrl(downloadUrl, pluginKey, version); 324 | } 325 | 326 | private static string GetFileNameFromUrl(string downloadUrl, string? pluginKey, string? version) 327 | { 328 | try 329 | { 330 | var uri = new Uri(downloadUrl); 331 | var urlFileName = Path.GetFileName(uri.LocalPath); 332 | 333 | if (!string.IsNullOrWhiteSpace(urlFileName) && Path.HasExtension(urlFileName)) 334 | { 335 | return urlFileName; 336 | } 337 | 338 | if (uri.Host == "marketplace.atlassian.com" && (uri.LocalPath.StartsWith("/files/") || uri.LocalPath.StartsWith("/download/"))) 339 | { 340 | return $"{pluginKey ?? "plugin"}-{version ?? "unknown"}.jar"; 341 | } 342 | 343 | return !string.IsNullOrWhiteSpace(urlFileName) ? urlFileName : $"{pluginKey ?? "plugin"}-{version ?? "unknown"}.jar"; 344 | } 345 | catch 346 | { 347 | return $"{pluginKey ?? "plugin"}-{version ?? "unknown"}.jar"; 348 | } 349 | } 350 | 351 | private async Task DownloadPluginFile(string downloadUrl, string outputFile, DownloaderSettings settings, CancellationToken cancellationToken) 352 | { 353 | for (int attempt = 1; attempt <= settings.MaxRetries; attempt++) 354 | { 355 | try 356 | { 357 | if (File.Exists(outputFile) && !settings.SkipFileCheck) 358 | { 359 | var localFileSize = new FileInfo(outputFile).Length; 360 | using var headRequest = new HttpRequestMessage(HttpMethod.Head, downloadUrl); 361 | var headResponse = await _client.SendAsync(headRequest, cancellationToken); 362 | 363 | if (headResponse.IsSuccessStatusCode && headResponse.Content.Headers.ContentLength.HasValue) 364 | { 365 | var remoteFileSize = headResponse.Content.Headers.ContentLength.Value; 366 | if (remoteFileSize == localFileSize) 367 | { 368 | _logger.LogInformation("File sizes match ({size} bytes). Skipping download of {outputFile}", remoteFileSize, outputFile); 369 | return; 370 | } 371 | else 372 | { 373 | _logger.LogWarning("File sizes differ (remote: {remoteSize}, local: {localSize}). Re-downloading {outputFile}", remoteFileSize, localFileSize, outputFile); 374 | File.Delete(outputFile); 375 | } 376 | } 377 | } 378 | 379 | using var outputStream = File.OpenWrite(outputFile); 380 | using var request = await _client.GetStreamAsync(downloadUrl, cancellationToken).ConfigureAwait(false); 381 | await request.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); 382 | 383 | _logger.LogInformation("File successfully downloaded to {outputFile}", outputFile); 384 | return; // Success, exit the method 385 | } 386 | // MODIFIED: Added specific catch for 404/400 errors 387 | catch (HttpRequestException httpEx) when (httpEx.StatusCode == System.Net.HttpStatusCode.NotFound || httpEx.StatusCode == System.Net.HttpStatusCode.BadRequest) 388 | { 389 | _logger.LogWarning("Download link for {file} is broken or unavailable (HTTP {statusCode}). Skipping this file.", outputFile, httpEx.StatusCode); 390 | 391 | // This is a final failure for this file. We should not retry. Exit the method. 392 | // First, ensure the failed partial file is deleted. 393 | try { if (File.Exists(outputFile)) File.Delete(outputFile); } catch { /* Ignore */ } 394 | return; 395 | } 396 | catch (OperationCanceledException) 397 | { 398 | _logger.LogWarning("Download of {outputFile} was canceled by the user.", outputFile); 399 | try { File.Delete(outputFile); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete incomplete file {outputFile}", outputFile); } 400 | throw; 401 | } 402 | catch (Exception ex) 403 | { 404 | _logger.LogError(ex, "Attempt {attempt} failed to download {url} to {outputFile}", attempt, downloadUrl, outputFile); 405 | if (attempt == settings.MaxRetries) 406 | { 407 | try { File.Delete(outputFile); } catch (Exception removeEx) { _logger.LogError(removeEx, "Failed to remove incomplete file {outputFile}", outputFile); } 408 | throw; 409 | } 410 | else 411 | { 412 | await Task.Delay(settings.DelayBetweenRetries, cancellationToken).ConfigureAwait(false); 413 | } 414 | } 415 | } 416 | } 417 | 418 | #endregion 419 | 420 | #region Private Helpers - Products 421 | 422 | private async Task<(string json, IDictionary versions)> GetJson(string feedUrl, string? productVersion, CancellationToken cancellationToken) 423 | { 424 | var atlassianJson = await _client.GetStringAsync(feedUrl, cancellationToken).ConfigureAwait(false); 425 | const string dlPrefix = "downloads("; 426 | var json = atlassianJson.StartsWith(dlPrefix) ? atlassianJson.Trim()[dlPrefix.Length..^1] : atlassianJson; 427 | _logger.LogTrace("Downloaded json: {json}", json); 428 | var parsed = JsonSerializer.Deserialize(json, jsonOptions)!; 429 | _logger.LogDebug("Found {releaseCount} releases", parsed.Length); 430 | var versions = parsed 431 | .GroupBy(a => a.Version) 432 | .Where(a => productVersion is null || a.Key == productVersion) 433 | .ToDictionary(a => a.Key, a => a.ToArray()); 434 | _logger.LogDebug("Found {releaseCount} releases", versions.Count); 435 | return (json, versions); 436 | } 437 | 438 | private IReadOnlyList GetFeedUrls(Uri[]? customFeed) => customFeed != null 439 | ? customFeed.Select(a => a.ToString()).ToArray() 440 | : SourceInformation.AtlassianSources; 441 | 442 | private async Task DownloadFilesFromFeed(string feedUrl, IDictionary versions, DownloaderSettings settings, CancellationToken cancellationToken) 443 | { 444 | var feedDir = Path.Combine(settings.OutputDir, feedUrl.Split('/').Last().Replace(".json", "")); 445 | _logger.LogInformation("Download from JSON \"{feedUrl}\" started", feedUrl); 446 | foreach (var version in versions) 447 | { 448 | cancellationToken.ThrowIfCancellationRequested(); 449 | var directory = Path.Combine(feedDir, version.Key); 450 | if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); 451 | 452 | foreach (var file in version.Value) 453 | { 454 | if (file.ZipUrl is null) continue; 455 | var serverPath = file.ZipUrl.PathAndQuery; 456 | var outputFile = Path.Combine(directory, serverPath.Split('/').Last()); 457 | 458 | if (File.Exists(outputFile) && settings.SkipFileCheck) 459 | { 460 | _logger.LogWarning("File \"{outputFile}\" already exists. Download skipped.", outputFile); 461 | continue; 462 | } 463 | if (settings.RandomizeDelay) 464 | { 465 | var delay = Random.Shared.Next(settings.MinDelay, settings.MaxDelay); 466 | _logger.LogDebug("Throttling: Waiting {delay}ms before next file...", delay); 467 | await Task.Delay(delay, cancellationToken).ConfigureAwait(false); 468 | } 469 | await DownloadFile(file, outputFile, settings, cancellationToken); 470 | } 471 | } 472 | _logger.LogInformation("All files from \"{feedUrl}\" successfully downloaded.", feedUrl); 473 | } 474 | 475 | private async Task DownloadFile(ResponseItem file, string outputFile, DownloaderSettings settings, CancellationToken cancellationToken) 476 | { 477 | for (int attempt = 1; attempt <= settings.MaxRetries; attempt++) 478 | { 479 | try 480 | { 481 | if (File.Exists(outputFile) && !settings.SkipFileCheck) 482 | { 483 | var localFileSize = new FileInfo(outputFile).Length; 484 | using var headRequest = new HttpRequestMessage(HttpMethod.Head, file.ZipUrl); 485 | var headResponse = await _client.SendAsync(headRequest, cancellationToken); 486 | 487 | if (headResponse.IsSuccessStatusCode && headResponse.Content.Headers.ContentLength.HasValue) 488 | { 489 | var remoteFileSize = headResponse.Content.Headers.ContentLength.Value; 490 | if (remoteFileSize == localFileSize) 491 | { 492 | _logger.LogInformation("Size of remote and local files are same for {outputFile}. Skipping.", outputFile); 493 | return; 494 | } 495 | else 496 | { 497 | _logger.LogWarning("Size of remote and local files are not same for {outputFile}. Re-downloading.", outputFile); 498 | File.Delete(outputFile); 499 | } 500 | } 501 | } 502 | 503 | if (!string.IsNullOrEmpty(file.Md5)) 504 | { 505 | await File.WriteAllTextAsync(outputFile + ".md5", file.Md5, cancellationToken); 506 | } 507 | 508 | using var outputStream = File.OpenWrite(outputFile); 509 | using var request = await _client.GetStreamAsync(file.ZipUrl!, cancellationToken); 510 | await request.CopyToAsync(outputStream, cancellationToken); 511 | _logger.LogInformation("File \"{uri}\" successfully downloaded to \"{outputFile}\".", file.ZipUrl, outputFile); 512 | return; 513 | } 514 | catch (OperationCanceledException) 515 | { 516 | _logger.LogWarning("Download of \"{uri}\" was canceled by the user.", file.ZipUrl); 517 | try { File.Delete(outputFile); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete incomplete file {outputFile}", outputFile); } 518 | throw; 519 | } 520 | catch (Exception ex) 521 | { 522 | _logger.LogError(ex, "Attempt {attempt} failed to download file \"{uri}\".", attempt, file.ZipUrl); 523 | 524 | if (attempt == settings.MaxRetries) 525 | { 526 | try { File.Delete(outputFile); } catch (Exception removeEx) { _logger.LogError(removeEx, "Failed to remove incomplete file \"{outputFile}\".", outputFile); } 527 | throw; 528 | } 529 | else 530 | { 531 | int delay; 532 | if (settings.RandomizeDelay) 533 | { 534 | delay = Random.Shared.Next(settings.MinDelay, settings.MaxDelay); 535 | _logger.LogDebug("Retry delay: Waiting random {delay}ms", delay); 536 | } 537 | else 538 | { 539 | delay = settings.DelayBetweenRetries; 540 | } 541 | 542 | await Task.Delay(delay, cancellationToken).ConfigureAwait(false); 543 | } 544 | } 545 | } 546 | } 547 | #endregion 548 | } -------------------------------------------------------------------------------- /src/Atlassian.Downloader.Console/Models/UserAgents.cs: -------------------------------------------------------------------------------- 1 | // Atlassian.Downloader.Console/Models/UserAgents.cs 2 | using System; 3 | 4 | namespace EpicMorg.Atlassian.Downloader.Models; 5 | 6 | public static class UserAgents 7 | { 8 | public static string GetRandom() => List[Random.Shared.Next(List.Length)]; 9 | 10 | public static readonly string[] List = new[] 11 | { 12 | // --- Windows --- 13 | "Mozilla/5.0 (Windows; U; Windows NT 6.2; x64; en-US) AppleWebKit/536.42 (KHTML, like Gecko) Chrome/51.0.3014.247 Safari/537.1 Edge/15.95600", 14 | "Mozilla/5.0 (Windows; Windows NT 10.1; x64) AppleWebKit/534.9 (KHTML, like Gecko) Chrome/52.0.3633.273 Safari/534.0 Edge/14.83488", 15 | "Mozilla/5.0 (Windows NT 10.4;; en-US) AppleWebKit/535.45 (KHTML, like Gecko) Chrome/50.0.2103.280 Safari/533.9 Edge/9.79704", 16 | "Mozilla/5.0 (Windows; U; Windows NT 10.3; WOW64) Gecko/20130401 Firefox/70.8", 17 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows; Windows NT 6.2; Win64; x64; en-US Trident/6.0)", 18 | "Mozilla/5.0 (compatible; MSIE 11.0; Windows; Windows NT 6.0; WOW64; en-US Trident/7.0)", 19 | "Mozilla/5.0 (Windows NT 6.0; x64; en-US) Gecko/20100101 Firefox/57.6", 20 | "Mozilla/5.0 (Windows; U; Windows NT 10.4; WOW64) Gecko/20130401 Firefox/58.4", 21 | "Mozilla/5.0 (Windows NT 10.5; Win64; x64; en-US) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/49.0.3271.251 Safari/537.4 Edge/16.33836", 22 | "Mozilla/5.0 (Windows NT 10.3; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/52.0.1524.143 Safari/603.6 Edge/8.85463", 23 | "Mozilla/5.0 (Windows; U; Windows NT 6.3; Win64; x64) AppleWebKit/535.40 (KHTML, like Gecko) Chrome/47.0.1456.206 Safari/537", 24 | "Mozilla/5.0 (Windows NT 10.4; x64; en-US) Gecko/20100101 Firefox/45.1", 25 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows; Windows NT 6.1; WOW64; en-US Trident/4.0)", 26 | "Mozilla/5.0 (Windows; Windows NT 10.2; WOW64) AppleWebKit/600.49 (KHTML, like Gecko) Chrome/55.0.2104.383 Safari/536.4 Edge/11.35077", 27 | "Mozilla/5.0 (Windows NT 10.1; x64) Gecko/20100101 Firefox/57.6", 28 | "Mozilla/5.0 (Windows; Windows NT 10.3; Win64; x64) AppleWebKit/533.45 (KHTML, like Gecko) Chrome/55.0.3421.349 Safari/537", 29 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Win64; x64; en-US Trident/5.0)", 30 | "Mozilla/5.0 (compatible; MSIE 11.0; Windows; Windows NT 6.2;; en-US Trident/7.0)", 31 | "Mozilla/5.0 (Windows NT 6.1;) AppleWebKit/602.6 (KHTML, like Gecko) Chrome/48.0.1528.127 Safari/537", 32 | "Mozilla/5.0 (Windows NT 10.4; WOW64) AppleWebKit/537.3 (KHTML, like Gecko) Chrome/52.0.3098.287 Safari/601.7 Edge/18.26584", 33 | "Mozilla/5.0 (Windows; U; Windows NT 6.0;; en-US) Gecko/20100101 Firefox/67.6", 34 | "Mozilla/5.0 (Windows NT 10.4; x64) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/53.0.3758.142 Safari/536", 35 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.3; WOW64 Trident/6.0)", 36 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; U; Windows NT 6.3; WOW64; en-US Trident/5.0)", 37 | "Mozilla/5.0 (Windows NT 10.1; x64; en-US) AppleWebKit/601.45 (KHTML, like Gecko) Chrome/54.0.1645.133 Safari/536.1 Edge/11.87147", 38 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 10.3; x64; en-US Trident/4.0)", 39 | "Mozilla/5.0 (Windows; Windows NT 10.2; x64) AppleWebKit/601.28 (KHTML, like Gecko) Chrome/48.0.2373.385 Safari/601.0 Edge/14.46797", 40 | "Mozilla/5.0 (Windows; Windows NT 10.0;; en-US) AppleWebKit/602.17 (KHTML, like Gecko) Chrome/47.0.1182.110 Safari/537", 41 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; U; Windows NT 6.2;; en-US Trident/5.0)", 42 | "Mozilla/5.0 (Windows NT 10.0; x64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/47.0.1559.246 Safari/601.9 Edge/15.43351", 43 | "Mozilla/5.0 (Windows; Windows NT 10.4;) AppleWebKit/601.36 (KHTML, like Gecko) Chrome/52.0.2412.134 Safari/536.9 Edge/18.27242", 44 | "Mozilla/5.0 (Windows; Windows NT 6.3; WOW64) AppleWebKit/603.29 (KHTML, like Gecko) Chrome/48.0.3105.345 Safari/537", 45 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows; Windows NT 6.0; x64 Trident/6.0)", 46 | "Mozilla/5.0 (Windows; U; Windows NT 10.4; Win64; x64) AppleWebKit/602.25 (KHTML, like Gecko) Chrome/55.0.1746.331 Safari/534.0 Edge/13.16582", 47 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; Windows NT 6.0; x64 Trident/5.0)", 48 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows; U; Windows NT 6.2; WOW64 Trident/6.0)", 49 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; U; Windows NT 6.1;; en-US Trident/5.0)", 50 | "Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.0; Win64; x64 Trident/7.0)", 51 | "Mozilla/5.0 (Windows; Windows NT 10.5; x64; en-US) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/49.0.2837.226 Safari/600", 52 | "Mozilla/5.0 (Windows; Windows NT 6.3; WOW64) AppleWebKit/603.38 (KHTML, like Gecko) Chrome/54.0.1382.147 Safari/534.8 Edge/11.70752", 53 | "Mozilla/5.0 (Windows; Windows NT 6.3; x64; en-US) AppleWebKit/603.3 (KHTML, like Gecko) Chrome/53.0.2229.276 Safari/601", 54 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; x64) Gecko/20100101 Firefox/46.2", 55 | "Mozilla/5.0 (Windows; U; Windows NT 10.3;) AppleWebKit/601.6 (KHTML, like Gecko) Chrome/53.0.2598.239 Safari/536.6 Edge/9.66671", 56 | "Mozilla/5.0 (Windows; Windows NT 6.1; x64) Gecko/20100101 Firefox/74.5", 57 | "Mozilla/5.0 (Windows; U; Windows NT 10.1; x64) AppleWebKit/535.31 (KHTML, like Gecko) Chrome/55.0.2698.308 Safari/600", 58 | "Mozilla/5.0 (Windows; U; Windows NT 10.4; Win64; x64; en-US) AppleWebKit/602.2 (KHTML, like Gecko) Chrome/55.0.3528.184 Safari/603", 59 | "Mozilla/5.0 (compatible; MSIE 11.0; Windows; U; Windows NT 6.1; x64; en-US Trident/7.0)", 60 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; Windows NT 6.3; Trident/5.0)", 61 | "Mozilla/5.0 (Windows; Windows NT 6.3;) Gecko/20100101 Firefox/72.6", 62 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; Windows NT 6.3; Win64; x64; en-US Trident/5.0)", 63 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows; Windows NT 6.2; x64 Trident/4.0)", 64 | "Mozilla/5.0 (Windows; U; Windows NT 6.3;) AppleWebKit/601.40 (KHTML, like Gecko) Chrome/52.0.1753.284 Safari/600", 65 | "Mozilla/5.0 (Windows; U; Windows NT 10.1; Win64; x64; en-US) AppleWebKit/534.11 (KHTML, like Gecko) Chrome/47.0.3551.217 Safari/601.8 Edge/13.48967", 66 | "Mozilla/5.0 (Windows; U; Windows NT 10.4; Win64; x64; en-US) Gecko/20130401 Firefox/59.6", 67 | "Mozilla/5.0 (Windows; Windows NT 10.0; Win64; x64; en-US) AppleWebKit/600.45 (KHTML, like Gecko) Chrome/49.0.1812.289 Safari/534", 68 | "Mozilla/5.0 (Windows; U; Windows NT 10.5; Win64; x64) AppleWebKit/601.2 (KHTML, like Gecko) Chrome/51.0.3358.370 Safari/537.0 Edge/12.20034", 69 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64) Gecko/20130401 Firefox/74.1", 70 | "Mozilla/5.0 (Windows; U; Windows NT 10.1; WOW64) AppleWebKit/534.4 (KHTML, like Gecko) Chrome/54.0.3360.334 Safari/602", 71 | "Mozilla/5.0 (Windows; Windows NT 10.5; WOW64) Gecko/20100101 Firefox/51.8", 72 | "Mozilla/5.0 (Windows; Windows NT 6.3; Win64; x64; en-US) AppleWebKit/601.11 (KHTML, like Gecko) Chrome/51.0.1478.118 Safari/603", 73 | "Mozilla/5.0 (Windows; Windows NT 6.3; Win64; x64; en-US) Gecko/20100101 Firefox/72.0", 74 | "Mozilla/5.0 (Windows; Windows NT 10.2; x64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/51.0.1981.308 Safari/533.5 Edge/9.30576", 75 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64 Trident/6.0)", 76 | "Mozilla/5.0 (Windows NT 10.5; WOW64) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/47.0.1595.302 Safari/601.5 Edge/9.34209", 77 | "Mozilla/5.0 (Windows; U; Windows NT 10.0; x64) AppleWebKit/537.23 (KHTML, like Gecko) Chrome/51.0.3243.172 Safari/600.6 Edge/11.21392", 78 | "Mozilla/5.0 (Windows; Windows NT 10.5; Win64; x64; en-US) AppleWebKit/535.15 (KHTML, like Gecko) Chrome/53.0.1796.120 Safari/602", 79 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; Windows NT 6.1; WOW64 Trident/5.0)", 80 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows; U; Windows NT 6.2;; en-US Trident/6.0)", 81 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows; Windows NT 6.0; Win64; x64 Trident/4.0)", 82 | "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; en-US Trident/4.0)", 83 | "Mozilla/5.0 (Windows NT 6.2; WOW64; en-US) AppleWebKit/602.33 (KHTML, like Gecko) Chrome/53.0.1760.185 Safari/533", 84 | "Mozilla/5.0 (Windows; Windows NT 10.0; WOW64) AppleWebKit/601.45 (KHTML, like Gecko) Chrome/47.0.2842.248 Safari/602.5 Edge/13.47432", 85 | "Mozilla/5.0 (Windows; U; Windows NT 10.3;) AppleWebKit/536.22 (KHTML, like Gecko) Chrome/54.0.3970.126 Safari/603.3 Edge/18.91862", 86 | "Mozilla/5.0 (Windows NT 10.2; x64) Gecko/20130401 Firefox/45.3", 87 | "Mozilla/5.0 (Windows; Windows NT 10.1; WOW64; en-US) AppleWebKit/603.1 (KHTML, like Gecko) Chrome/53.0.2670.245 Safari/600.6 Edge/18.19804", 88 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows; Windows NT 6.1; Trident/5.0)", 89 | "Mozilla/5.0 (Windows; U; Windows NT 10.4;; en-US) AppleWebKit/601.24 (KHTML, like Gecko) Chrome/52.0.3880.259 Safari/600", 90 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.0; x64; en-US Trident/6.0)", 91 | "Mozilla/5.0 (Windows NT 10.1; x64) Gecko/20130401 Firefox/66.6", 92 | "Mozilla/5.0 (compatible; MSIE 11.0; Windows; U; Windows NT 6.1; x64 Trident/7.0)", 93 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.3; Win64; x64; en-US Trident/5.0)", 94 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows; U; Windows NT 6.0; x64 Trident/4.0)", 95 | "Mozilla/5.0 (compatible; MSIE 7.0; Windows; U; Windows NT 6.3;; en-US Trident/4.0)", 96 | "Mozilla/5.0 (Windows; Windows NT 10.1; WOW64) Gecko/20100101 Firefox/69.9", 97 | "Mozilla/5.0 (Windows NT 10.2; x64; en-US) AppleWebKit/533.31 (KHTML, like Gecko) Chrome/53.0.1232.335 Safari/536.9 Edge/13.71354", 98 | "Mozilla/5.0 (Windows NT 10.3;) Gecko/20100101 Firefox/70.7", 99 | "Mozilla/5.0 (Windows NT 10.3; x64; en-US) AppleWebKit/601.48 (KHTML, like Gecko) Chrome/48.0.3352.384 Safari/603", 100 | "Mozilla/5.0 (Windows; U; Windows NT 10.5; x64) AppleWebKit/603.7 (KHTML, like Gecko) Chrome/52.0.2636.283 Safari/537.0 Edge/8.92089", 101 | "Mozilla/5.0 (Windows; Windows NT 6.2; x64; en-US) AppleWebKit/600.21 (KHTML, like Gecko) Chrome/47.0.3213.254 Safari/533", 102 | "Mozilla/5.0 (Windows; U; Windows NT 10.1; x64; en-US) AppleWebKit/535.23 (KHTML, like Gecko) Chrome/48.0.1290.137 Safari/535.0 Edge/11.40291", 103 | "Mozilla/5.0 (Windows NT 6.3; x64) Gecko/20100101 Firefox/47.2", 104 | "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; en-US Trident/4.0)", 105 | "Mozilla/5.0 (Windows; Windows NT 10.3;) AppleWebKit/600.8 (KHTML, like Gecko) Chrome/53.0.2670.290 Safari/600", 106 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows; U; Windows NT 6.0; x64 Trident/4.0)", 107 | "Mozilla/5.0 (Windows NT 10.4; WOW64; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/54.0.1524.102 Safari/602.9 Edge/15.97549", 108 | "Mozilla/5.0 (Windows; U; Windows NT 6.3; WOW64; en-US) AppleWebKit/600.15 (KHTML, like Gecko) Chrome/47.0.1556.130 Safari/600", 109 | 110 | // --- macOS --- 111 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 8_1_3; en-US) AppleWebKit/601.43 (KHTML, like Gecko) Chrome/50.0.1747.283 Safari/602", 112 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_6; en-US) Gecko/20100101 Firefox/49.6", 113 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_9_4; en-US) Gecko/20100101 Firefox/63.9", 114 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 8_8_7) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/54.0.3826.348 Safari/600", 115 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_0_0; en-US) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.1726.299 Safari/534", 116 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_12_1) Gecko/20130401 Firefox/58.1", 117 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 8_3_3) Gecko/20100101 Firefox/68.7", 118 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_5) AppleWebKit/600.43 (KHTML, like Gecko) Chrome/50.0.1497.376 Safari/600", 119 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_2_2) Gecko/20130401 Firefox/49.9", 120 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 8_7_3) AppleWebKit/534.50 (KHTML, like Gecko) Chrome/55.0.1455.310 Safari/603", 121 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 8_3_0; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/51.0.1135.180 Safari/600", 122 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 9_6_3) Gecko/20130401 Firefox/63.5", 123 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_0_5; en-US) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/52.0.2896.376 Safari/534", 124 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2) AppleWebKit/533.24 (KHTML, like Gecko) Chrome/51.0.1338.310 Safari/603", 125 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_3_3; en-US) AppleWebKit/534.50 (KHTML, like Gecko) Chrome/52.0.3889.105 Safari/534", 126 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_9_1; en-US) AppleWebKit/603.4 (KHTML, like Gecko) Chrome/47.0.3835.126 Safari/603", 127 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_2_1) AppleWebKit/602.39 (KHTML, like Gecko) Chrome/55.0.1992.342 Safari/534", 128 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_0_1) Gecko/20100101 Firefox/60.5", 129 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_2_3) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/53.0.3389.265 Safari/535", 130 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_9) Gecko/20100101 Firefox/65.4", 131 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 8_4_0) Gecko/20100101 Firefox/70.5", 132 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_0) Gecko/20100101 Firefox/52.1", 133 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_8_2; en-US) AppleWebKit/601.10 (KHTML, like Gecko) Chrome/48.0.2732.108 Safari/533", 134 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_2_7) Gecko/20100101 Firefox/49.2", 135 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_7_6; en-US) Gecko/20100101 Firefox/65.3", 136 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 9_3_4) Gecko/20100101 Firefox/55.5", 137 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 7_5_0; en-US) AppleWebKit/536.30 (KHTML, like Gecko) Chrome/53.0.3683.359 Safari/534", 138 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 9_6_1) AppleWebKit/600.50 (KHTML, like Gecko) Chrome/50.0.1514.253 Safari/600", 139 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 8_6_7; en-US) Gecko/20100101 Firefox/73.4", 140 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_10_3; en-US) AppleWebKit/537.48 (KHTML, like Gecko) Chrome/47.0.1072.369 Safari/536", 141 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/601.5 (KHTML, like Gecko) Chrome/50.0.3725.178 Safari/602", 142 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 9_3_6) AppleWebKit/600.43 (KHTML, like Gecko) Chrome/50.0.1437.293 Safari/533", 143 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_7; en-US) Gecko/20100101 Firefox/51.7", 144 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_7_2) AppleWebKit/537.29 (KHTML, like Gecko) Chrome/52.0.3535.277 Safari/600", 145 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_12_0; en-US) Gecko/20130401 Firefox/67.7", 146 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_9_9; en-US) Gecko/20100101 Firefox/50.3", 147 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 8_4_2) Gecko/20100101 Firefox/47.2", 148 | 149 | // --- Linux --- 150 | "Mozilla/5.0 (Linux; U; Linux i574 x86_64; en-US) AppleWebKit/602.17 (KHTML, like Gecko) Chrome/49.0.2537.390 Safari/537", 151 | "Mozilla/5.0 (Linux x86_64; en-US) AppleWebKit/601.41 (KHTML, like Gecko) Chrome/53.0.2594.366 Safari/536", 152 | "Mozilla/5.0 (Linux; U; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) Chrome/51.0.2341.372 Safari/601", 153 | "Mozilla/5.0 (Linux; Linux i563 x86_64) Gecko/20100101 Firefox/59.3", 154 | "Mozilla/5.0 (Linux x86_64; en-US) AppleWebKit/533.9 (KHTML, like Gecko) Chrome/49.0.2810.347 Safari/533", 155 | "Mozilla/5.0 (Linux x86_64; en-US) Gecko/20100101 Firefox/53.6", 156 | "Mozilla/5.0 (Linux; Linux i675 x86_64; en-US) AppleWebKit/600.6 (KHTML, like Gecko) Chrome/53.0.2919.253 Safari/536", 157 | "Mozilla/5.0 (Linux; U; Linux x86_64) Gecko/20100101 Firefox/47.9", 158 | "Mozilla/5.0 (Linux; Linux i570 x86_64) Gecko/20100101 Firefox/54.4", 159 | "Mozilla/5.0 (Linux; Linux i671 ; en-US) AppleWebKit/534.45 (KHTML, like Gecko) Chrome/47.0.1287.146 Safari/603", 160 | "Mozilla/5.0 (U; Linux x86_64) Gecko/20100101 Firefox/47.9", 161 | "Mozilla/5.0 (Linux; Linux i545 ) AppleWebKit/533.47 (KHTML, like Gecko) Chrome/55.0.1842.291 Safari/533", 162 | "Mozilla/5.0 (Linux; U; Linux x86_64; en-US) AppleWebKit/600.39 (KHTML, like Gecko) Chrome/49.0.1302.314 Safari/534", 163 | "Mozilla/5.0 (Linux; Linux i562 ) AppleWebKit/601.34 (KHTML, like Gecko) Chrome/55.0.3254.284 Safari/603", 164 | "Mozilla/5.0 (Linux; U; Linux i664 x86_64; en-US) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/49.0.2773.233 Safari/603", 165 | "Mozilla/5.0 (U; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/47.0.1170.368 Safari/535", 166 | "Mozilla/5.0 (U; Linux x86_64) AppleWebKit/603.41 (KHTML, like Gecko) Chrome/55.0.3543.315 Safari/600", 167 | "Mozilla/5.0 (Linux i663 x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/49.0.2286.241 Safari/603", 168 | "Mozilla/5.0 (Linux; Linux i676 ) AppleWebKit/600.27 (KHTML, like Gecko) Chrome/47.0.2897.307 Safari/600", 169 | "Mozilla/5.0 (U; Linux i554 x86_64; en-US) Gecko/20100101 Firefox/52.0", 170 | "Mozilla/5.0 (Linux i580 x86_64; en-US) AppleWebKit/533.46 (KHTML, like Gecko) Chrome/49.0.2301.143 Safari/535", 171 | "Mozilla/5.0 (Linux; Linux i643 ) AppleWebKit/602.47 (KHTML, like Gecko) Chrome/54.0.1213.231 Safari/601", 172 | "Mozilla/5.0 (Linux i553 ; en-US) AppleWebKit/537.48 (KHTML, like Gecko) Chrome/53.0.2809.165 Safari/601", 173 | "Mozilla/5.0 (Linux x86_64; en-US) AppleWebKit/536.20 (KHTML, like Gecko) Chrome/47.0.3293.240 Safari/535", 174 | "Mozilla/5.0 (U; Linux i650 ; en-US) Gecko/20130401 Firefox/63.2", 175 | "Mozilla/5.0 (Linux x86_64) Gecko/20130401 Firefox/58.1", 176 | "Mozilla/5.0 (U; Linux i546 ) Gecko/20100101 Firefox/73.5", 177 | "Mozilla/5.0 (U; Linux i540 ) AppleWebKit/534.46 (KHTML, like Gecko) Chrome/49.0.1830.121 Safari/601", 178 | "Mozilla/5.0 (Linux; U; Linux x86_64; en-US) AppleWebKit/536.45 (KHTML, like Gecko) Chrome/50.0.2413.259 Safari/600", 179 | "Mozilla/5.0 (U; Linux x86_64; en-US) AppleWebKit/535.36 (KHTML, like Gecko) Chrome/53.0.3424.223 Safari/536", 180 | "Mozilla/5.0 (Linux; Linux i655 x86_64) Gecko/20130401 Firefox/65.5", 181 | 182 | // --- Android --- 183 | "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7X Build/MDB08L) AppleWebKit/602.18 (KHTML, like Gecko) Chrome/54.0.2501.184 Mobile Safari/534.8", 184 | "Mozilla/5.0 (Linux; U; Android 6.0; HTC One801e dual sim Build/MRA58K) AppleWebKit/533.6 (KHTML, like Gecko) Chrome/49.0.2083.124 Mobile Safari/535.7", 185 | "Mozilla/5.0 (Linux; U; Android 5.0.1; HTC [M8|M9|M8 Pro Build/LRX22G) AppleWebKit/536.30 (KHTML, like Gecko) Chrome/50.0.3434.145 Mobile Safari/603.6", 186 | "Mozilla/5.0 (Linux; Android 6.0.1; HTC One0P8B2 Build/MRA58K) AppleWebKit/601.28 (KHTML, like Gecko) Chrome/52.0.3492.103 Mobile Safari/601.8", 187 | "Mozilla/5.0 (Linux; U; Android 7.1; LG-H900 Build/NRD90M) AppleWebKit/600.15 (KHTML, like Gecko) Chrome/50.0.3706.178 Mobile Safari/536.0", 188 | "Mozilla/5.0 (Android; Android 5.1; MOTOROLA MOTO XT1570 MOTO X STYLE Build/LPK23) AppleWebKit/600.47 (KHTML, like Gecko) Chrome/52.0.3419.246 Mobile Safari/535.4", 189 | "Mozilla/5.0 (Linux; Android 7.1; Xperia Build/NDE63X) AppleWebKit/533.32 (KHTML, like Gecko) Chrome/51.0.1633.342 Mobile Safari/603.9", 190 | "Mozilla/5.0 (Linux; Android 7.1; Xperia V Build/NDE63X) AppleWebKit/602.19 (KHTML, like Gecko) Chrome/54.0.3147.358 Mobile Safari/601.8", 191 | "Mozilla/5.0 (Android; Android 4.4.1; IQ4500 Quad Build/KOT49H) AppleWebKit/602.2 (KHTML, like Gecko) Chrome/50.0.1442.181 Mobile Safari/537.4", 192 | "Mozilla/5.0 (Linux; U; Android 4.4; [HM NOTE|NOTE-III|NOTE2 1LTET) AppleWebKit/603.41 (KHTML, like Gecko) Chrome/53.0.1668.241 Mobile Safari/537.6", 193 | "Mozilla/5.0 (Android; Android 5.0.2; SM-P555 Build/LRX22G) AppleWebKit/600.9 (KHTML, like Gecko) Chrome/51.0.3848.139 Mobile Safari/537.9", 194 | "Mozilla/5.0 (Linux; Android 5.0.2; LG-D724 Build/LRX22G) AppleWebKit/535.33 (KHTML, like Gecko) Chrome/48.0.1627.174 Mobile Safari/533.9", 195 | "Mozilla/5.0 (Linux; U; Android 5.1.1; SM-G9350FQ Build/MMB29M) AppleWebKit/536.25 (KHTML, like Gecko) Chrome/47.0.1290.337 Mobile Safari/601.4", 196 | "Mozilla/5.0 (Linux; U; Android 4.4.4; XT1034 Build/[KXB20.9|KXC21.5]) AppleWebKit/534.4 (KHTML, like Gecko) Chrome/49.0.2692.118 Mobile Safari/602.3", 197 | "Mozilla/5.0 (Linux; Android 6.0.1; HTC OneS Build/MRA58K) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/55.0.2559.319 Mobile Safari/534.8", 198 | "Mozilla/5.0 (Android; Android 6.0; Nexus 6X Build/MMB29K) AppleWebKit/601.14 (KHTML, like Gecko) Chrome/51.0.3835.371 Mobile Safari/536.0", 199 | "Mozilla/5.0 (Linux; U; Android 4.4.1; LG-V700 Build/KOT49I) AppleWebKit/603.37 (KHTML, like Gecko) Chrome/49.0.3670.289 Mobile Safari/537.7", 200 | "Mozilla/5.0 (Linux; Android 4.4; SAMSUNG SM-N8000 Build/KVT49L) AppleWebKit/535.50 (KHTML, like Gecko) Chrome/52.0.3728.232 Mobile Safari/534.1", 201 | "Mozilla/5.0 (Linux; Android 4.4.1; SM-E500L Build/KTU84P) AppleWebKit/603.39 (KHTML, like Gecko) Chrome/52.0.1698.371 Mobile Safari/603.0", 202 | "Mozilla/5.0 (Linux; U; Android 4.4.4; SAMSUNG SM-J100F Build/KTU84P) AppleWebKit/600.35 (KHTML, like Gecko) Chrome/50.0.2867.215 Mobile Safari/535.6", 203 | "Mozilla/5.0 (Linux; U; Android 5.1.1; SM-G925M Build/LMY47X) AppleWebKit/533.43 (KHTML, like Gecko) Chrome/47.0.3241.116 Mobile Safari/534.3", 204 | "Mozilla/5.0 (Android; Android 5.0; SAMSUNG SM-G900F Build/LRX21T) AppleWebKit/601.15 (KHTML, like Gecko) Chrome/47.0.3902.262 Mobile Safari/537.3", 205 | "Mozilla/5.0 (Linux; Android 4.4; SAMSUNG SM-N8010 Build/KVT49L) AppleWebKit/601.31 (KHTML, like Gecko) Chrome/49.0.1884.346 Mobile Safari/535.2", 206 | "Mozilla/5.0 (Android; Android 7.0; Xperia Build/NDE63X) AppleWebKit/533.47 (KHTML, like Gecko) Chrome/48.0.2580.191 Mobile Safari/600.1", 207 | "Mozilla/5.0 (Linux; U; Android 5.0; LG-G801T Build/KVT49L) AppleWebKit/601.45 (KHTML, like Gecko) Chrome/55.0.1802.284 Mobile Safari/536.9", 208 | "Mozilla/5.0 (Android; Android 4.4.1; HTC One_M8 dual sim Build/KTU84L) AppleWebKit/534.49 (KHTML, like Gecko) Chrome/47.0.2408.197 Mobile Safari/600.2", 209 | "Mozilla/5.0 (Linux; U; Android 4.4.1; XT1047 Build/[KXB20.9|KXC21.5]) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/53.0.2791.282 Mobile Safari/603.3", 210 | "Mozilla/5.0 (Linux; U; Android 4.3.1; SAMSUNG SM-T265v Build/JSS15J) AppleWebKit/535.49 (KHTML, like Gecko) Chrome/55.0.1376.288 Mobile Safari/536.6", 211 | "Mozilla/5.0 (Android; Android 5.0.1; Nokia 1000 4G Build/GRK39F) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/54.0.3578.157 Mobile Safari/535.2", 212 | "Mozilla/5.0 (Android; Android 4.4.1; Nexus_S_4G Build/GRJ22) AppleWebKit/533.44 (KHTML, like Gecko) Chrome/55.0.2373.170 Mobile Safari/602.4", 213 | "Mozilla/5.0 (Android; Android 5.1; SAMSUNG SM-G9350S Build/MMB29M) AppleWebKit/601.44 (KHTML, like Gecko) Chrome/54.0.2751.190 Mobile Safari/601.4", 214 | "Mozilla/5.0 (Android; Android 7.0; LG-H900 Build/NRD90M) AppleWebKit/602.34 (KHTML, like Gecko) Chrome/47.0.1888.256 Mobile Safari/534.0", 215 | "Mozilla/5.0 (Linux; U; Android 5.0.2; HTC [M8|M9|M8 Pro Build/LRX22G) AppleWebKit/602.50 (KHTML, like Gecko) Chrome/52.0.3773.191 Mobile Safari/603.0", 216 | "Mozilla/5.0 (Linux; U; Android 6.0; HTC Onemini 2 dual sim Build/MRA58K) AppleWebKit/603.23 (KHTML, like Gecko) Chrome/51.0.3816.280 Mobile Safari/533.0", 217 | "Mozilla/5.0 (Linux; U; Android 6.0.1; Nexus 6X Build/MDB08L) AppleWebKit/602.28 (KHTML, like Gecko) Chrome/54.0.1972.343 Mobile Safari/536.5", 218 | 219 | // --- iOS (iPhone, iPad, iPod) --- 220 | "Mozilla/5.0 (iPhone; CPU iPhone OS 7_6_4; like Mac OS X) AppleWebKit/601.23 (KHTML, like Gecko) Chrome/55.0.3953.357 Mobile Safari/534.6", 221 | "Mozilla/5.0 (iPhone; CPU iPhone OS 7_8_7; like Mac OS X) AppleWebKit/536.17 (KHTML, like Gecko) Chrome/47.0.3761.128 Mobile Safari/600.8", 222 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_6_4; like Mac OS X) AppleWebKit/601.48 (KHTML, like Gecko) Chrome/55.0.2115.393 Mobile Safari/603.9", 223 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_4_1; like Mac OS X) AppleWebKit/601.11 (KHTML, like Gecko) Chrome/50.0.2716.167 Mobile Safari/602.5", 224 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_5; like Mac OS X) AppleWebKit/600.35 (KHTML, like Gecko) Chrome/52.0.1755.142 Mobile Safari/603.1", 225 | "Mozilla/5.0 (iPod; CPU iPod OS 8_8_6; like Mac OS X) AppleWebKit/533.16 (KHTML, like Gecko) Chrome/55.0.3796.306 Mobile Safari/537.6", 226 | "Mozilla/5.0 (iPad; CPU iPad OS 9_4_9 like Mac OS X) AppleWebKit/536.2 (KHTML, like Gecko) Chrome/50.0.1641.324 Mobile Safari/534.4", 227 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_9_6; like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Chrome/50.0.3396.257 Mobile Safari/601.5", 228 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_6_2; like Mac OS X) AppleWebKit/537.50 (KHTML, like Gecko) Chrome/52.0.1867.137 Mobile Safari/537.2", 229 | "Mozilla/5.0 (iPad; CPU iPad OS 9_2_8 like Mac OS X) AppleWebKit/600.45 (KHTML, like Gecko) Chrome/47.0.3249.157 Mobile Safari/603.1", 230 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_6_1; like Mac OS X) AppleWebKit/600.2 (KHTML, like Gecko) Chrome/51.0.2316.248 Mobile Safari/536.3", 231 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_5_8; like Mac OS X) AppleWebKit/600.29 (KHTML, like Gecko) Chrome/51.0.3623.100 Mobile Safari/602.9", 232 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_6; like Mac OS X) AppleWebKit/537.33 (KHTML, like Gecko) Chrome/48.0.2260.175 Mobile Safari/600.8", 233 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_7_8; like Mac OS X) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/49.0.2114.397 Mobile Safari/534.2", 234 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_2_9; like Mac OS X) AppleWebKit/602.32 (KHTML, like Gecko) Chrome/47.0.3093.287 Mobile Safari/535.6", 235 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_7_0; like Mac OS X) AppleWebKit/533.35 (KHTML, like Gecko) Chrome/48.0.1838.218 Mobile Safari/600.0", 236 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_9_0; like Mac OS X) AppleWebKit/602.33 (KHTML, like Gecko) Chrome/51.0.3447.236 Mobile Safari/536.8", 237 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_9_5; like Mac OS X) AppleWebKit/602.32 (KHTML, like Gecko) Chrome/55.0.2029.180 Mobile Safari/536.7", 238 | "Mozilla/5.0 (iPad; CPU iPad OS 10_3_7 like Mac OS X) AppleWebKit/600.42 (KHTML, like Gecko) Chrome/55.0.1067.398 Mobile Safari/601.5", 239 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_8; like Mac OS X) AppleWebKit/533.37 (KHTML, like Gecko) Chrome/50.0.3040.317 Mobile Safari/602.7", 240 | "Mozilla/5.0 (iPhone; CPU iPhone OS 7_6_9; like Mac OS X) AppleWebKit/533.19 (KHTML, like Gecko) Chrome/49.0.2537.394 Mobile Safari/533.7", 241 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_9_1; like Mac OS X) AppleWebKit/603.11 (KHTML, like Gecko) Chrome/49.0.2863.140 Mobile Safari/537.8", 242 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3_2; like Mac OS X) AppleWebKit/600.29 (KHTML, like Gecko) Chrome/49.0.3978.261 Mobile Safari/534.0", 243 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_8_6; like Mac OS X) AppleWebKit/602.49 (KHTML, like Gecko) Chrome/55.0.3181.360 Mobile Safari/600.7", 244 | "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3; like Mac OS X) AppleWebKit/537.7 (KHTML, like Gecko) Chrome/51.0.2913.165 Mobile Safari/601.5", 245 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_8; like Mac OS X) AppleWebKit/535.17 (KHTML, like Gecko) Chrome/49.0.2010.351 Mobile Safari/603.4", 246 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_2_3; like Mac OS X) AppleWebKit/602.50 (KHTML, like Gecko) Chrome/50.0.2841.189 Mobile Safari/603.3", 247 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_6; like Mac OS X) AppleWebKit/535.5 (KHTML, like Gecko) Chrome/51.0.3985.145 Mobile Safari/536.5", 248 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_6_5; like Mac OS X) AppleWebKit/533.12 (KHTML, like Gecko) Chrome/53.0.2140.366 Mobile Safari/535.9", 249 | "Mozilla/5.0 (iPod; CPU iPod OS 10_6_8; like Mac OS X) AppleWebKit/535.36 (KHTML, like Gecko) Chrome/53.0.1908.206 Mobile Safari/537.7", 250 | "Mozilla/5.0 (iPod; CPU iPod OS 11_8_6; like Mac OS X) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/51.0.1067.334 Mobile Safari/603.6", 251 | "Mozilla/5.0 (iPad; CPU iPad OS 9_7_0 like Mac OS X) AppleWebKit/602.20 (KHTML, like Gecko) Chrome/55.0.3157.353 Mobile Safari/603.6", 252 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1_1; like Mac OS X) AppleWebKit/600.17 (KHTML, like Gecko) Chrome/53.0.2163.240 Mobile Safari/537.1", 253 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_9_1; like Mac OS X) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/52.0.3632.286 Mobile Safari/601.9", 254 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_8; like Mac OS X) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/55.0.2595.215 Mobile Safari/601.1", 255 | "Mozilla/5.0 (iPhone; CPU iPhone OS 7_6_1; like Mac OS X) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/54.0.2641.258 Mobile Safari/603.7", 256 | "Mozilla/5.0 (iPod; CPU iPod OS 8_1_9; like Mac OS X) AppleWebKit/600.48 (KHTML, like Gecko) Chrome/48.0.1953.211 Mobile Safari/601.7", 257 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1_5; like Mac OS X) AppleWebKit/602.28 (KHTML, like Gecko) Chrome/53.0.3130.278 Mobile Safari/536.5", 258 | "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_5; like Mac OS X) AppleWebKit/535.37 (KHTML, like Gecko) Chrome/48.0.1919.353 Mobile Safari/602.9", 259 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_9_5; like Mac OS X) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/53.0.1126.125 Mobile Safari/602.1", 260 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_9_6; like Mac OS X) AppleWebKit/602.36 (KHTML, like Gecko) Chrome/53.0.1269.226 Mobile Safari/601.9", 261 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_8; like Mac OS X) AppleWebKit/533.41 (KHTML, like Gecko) Chrome/48.0.2577.188 Mobile Safari/602.0", 262 | }; 263 | } --------------------------------------------------------------------------------