tools/Elastic.CommonSchema.Generator/SpecificationDownloader.cs (126 lines of code) (raw):
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Octokit;
using ShellProgressBar;
using static System.Text.Encoding;
namespace Elastic.CommonSchema.Generator
{
public static class SpecificationDownloader
{
private const string Core = "Core";
private const string Legacy = "legacy";
private const string Composable = "composable";
private const string Components = "component";
private static readonly ProgressBarOptions MainProgressBarOptions = new()
{
BackgroundColor = ConsoleColor.DarkGray, ForegroundColorError = ConsoleColor.Red
};
private static readonly Dictionary<string, string> OnlineSpecifications = new()
{
{ Core, "generated/ecs" },
{ Legacy, "generated/elasticsearch/legacy" },
{ Composable, "generated/elasticsearch/composable" },
{ Path.Combine(Composable, Components), "generated/elasticsearch/composable/component" }
};
private static readonly ProgressBarOptions SubProgressBarOptions = new()
{
ForegroundColor = ConsoleColor.Cyan,
ForegroundColorDone = ConsoleColor.DarkGreen,
ProgressCharacter = '─',
BackgroundColor = ConsoleColor.DarkGray
};
public static async Task DownloadAsync(string branch, string token)
{
var client = new GitHubClient(new ProductHeaderValue("ecs-generator"));
if (!string.IsNullOrEmpty(token))
client.Credentials = new Credentials(token);
using var queryProgress = new ProgressBar(OnlineSpecifications.Count, "Listing remote files", MainProgressBarOptions);
await WaitRateLimit(client, queryProgress);
var repo = await client.Repository.Get("elastic", "ecs");
var specifications = new List<Specification>();
foreach (var (folder, remotePath) in OnlineSpecifications)
{
await WaitRateLimit(client, queryProgress);
var contents = await client.Repository.Content.GetAllContentsByRef(repo.Id, remotePath, branch);
var spec = new Specification
{
FolderOnDisk = Path.Combine(branch, folder),
Branch = branch,
RemoteFiles = contents.Select(c => new RemoteFile(c.Name, c.Path)).ToArray()
};
specifications.Add(spec);
}
using var progress = new ProgressBar(specifications.Count, "Downloading specifications", MainProgressBarOptions);
foreach (var spec in specifications)
{
progress.Message = $"Downloading to {spec.FolderOnDisk} for branch {branch}";
await DownloadDefinitionsAsync(spec, client, progress, ".yml", ".json");
progress.Tick($"Downloaded to {spec.FolderOnDisk} for branch {branch}");
}
}
private static async Task WaitRateLimit(GitHubClient client, ProgressBar progressBar)
{
var apiInfo = client.GetLastApiInfo();
var rateLimit = apiInfo?.RateLimit ?? (await client.RateLimit.GetRateLimits()).Rate;
if (rateLimit.Remaining > 0) return;
var options = new ProgressBarOptions
{
ForegroundColor = ConsoleColor.Yellow,
ForegroundColorDone = ConsoleColor.DarkGreen,
BackgroundColor = ConsoleColor.DarkGray,
BackgroundCharacter = '\u2593'
};
var waitTime = rateLimit.Reset - DateTimeOffset.UtcNow;
using var indeterminate = progressBar.SpawnIndeterminate($"Github rate limit hit, waiting: {waitTime}", options);
await Task.Delay(waitTime);
indeterminate.Finished();
}
private static async Task DownloadDefinitionsAsync(Specification spec, GitHubClient client, ProgressBar progress,
params string[] filenameMatch
)
{
if (!Directory.Exists(CodeConfiguration.SpecificationFolder))
Directory.CreateDirectory(CodeConfiguration.SpecificationFolder);
var endpoints = spec.RemoteFiles.Where(r => filenameMatch.Any(m => r.FileName.EndsWith(m))).ToArray();
if (endpoints.Length == 0)
{
progress.WriteErrorLine($"No remote files found to download to: {spec.FolderOnDisk}");
return;
}
using var subBar = progress.Spawn(endpoints.Length, "fetching individual files", SubProgressBarOptions);
foreach (var endpoint in endpoints)
{
await WaitRateLimit(client, progress);
var bytes = await client.Repository.Content.GetRawContentByRef("elastic", "ecs", endpoint.Path, spec.Branch);
WriteToFolder(spec.FolderOnDisk, endpoint.FileName, UTF8.GetString(bytes));
subBar.Tick($"Downloading {endpoint.FileName}");
}
}
private static void WriteToFolder(string folder, string filename, string contents)
{
var f = Path.Combine(CodeConfiguration.SpecificationFolder, folder);
if (!Directory.Exists(f)) Directory.CreateDirectory(f);
var path = Path.Combine(f, filename);
File.WriteAllText(path, contents);
}
private readonly struct RemoteFile
{
public readonly string FileName;
public readonly string Path;
public RemoteFile(string fileName, string path)
{
FileName = fileName;
Path = path;
}
}
private class Specification
{
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string Branch { get; set; }
public string FolderOnDisk { get; set; }
public RemoteFile[] RemoteFiles { get; set; }
}
}
}