├── 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 | [](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 | # [](https://github.com/EpicMorg/atlassian-downloader/commits) [](https://github.com/EpicMorg/atlassian-downloader/issues) [](https://github.com/EpicMorg/atlassian-downloader/network) [](https://github.com/EpicMorg/atlassian-downloader/stargazers) [](https://github.com/EpicMorg/atlassian-downloader/archive/master.zip) [](https://github.com/EpicMorg/atlassian-downloader/releases) [](https://github.com/EpicMorg/atlassian-downloader/releases) [](LICENSE.md) [](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 | 
8 | 
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 [](https://github.com/EpicMorg/atlassian-downloader/releases) [](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` | [](https://chocolatey.org/packages/atlassian-downloader/) | [](https://chocolatey.org/packages/atlassian-downloader/)
41 |
42 | -------------------
43 |
44 | # Usage and settings
45 | ## CLI args
46 |
47 | 
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 | | [](https://www.atlassian.com/software/bamboo) | :white_check_mark: | :white_check_mark: | :white_check_mark: |
132 | | [&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/bitbucket) | :white_check_mark: | :white_check_mark: | :interrobang: |
133 | | [&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 | | [](https://www.atlassian.com/software/clover) | :white_check_mark: | :white_check_mark: | :x: |
135 | | [](https://www.atlassian.com/software/confluence) | :white_check_mark: | :white_check_mark: | :x: |
136 | | [](https://www.atlassian.com/software/crowd) | :white_check_mark: | :white_check_mark: | :x: |
137 | | [](https://www.atlassian.com/software/crucible) | :white_check_mark: | :white_check_mark: | :x: |
138 | | [](https://www.atlassian.com/software/fisheye) | :white_check_mark: | :white_check_mark: | :x: |
139 | | [](https://www.atlassian.com/software/jira/core) | :white_check_mark: | :white_check_mark: | :x: |
140 | | [](https://www.atlassian.com/software/jira) | :white_check_mark: | :white_check_mark: | :white_check_mark: |
141 | | [](https://www.atlassian.com/software/jira/service-management) | :white_check_mark: | :white_check_mark: | :white_check_mark: |
142 | | [](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 | }
--------------------------------------------------------------------------------