in src/Elastic.Markdown/Links/CrossLinks/CrossLinkFetcher.cs [34:151]
public abstract class CrossLinkFetcher(ILoggerFactory logger) : IDisposable
{
private readonly ILogger _logger = logger.CreateLogger(nameof(CrossLinkFetcher));
private readonly HttpClient _client = new();
private LinkReferenceRegistry? _linkIndex;
public static LinkReference Deserialize(string json) =>
JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LinkReference)!;
public abstract Task<FetchedCrossLinks> Fetch(Cancel ctx);
protected async Task<LinkReferenceRegistry> FetchLinkIndex(Cancel ctx)
{
if (_linkIndex is not null)
{
_logger.LogTrace("Using cached link index");
return _linkIndex;
}
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json";
_logger.LogInformation("Fetching {Url}", url);
var json = await _client.GetStringAsync(url, ctx);
_linkIndex = LinkReferenceRegistry.Deserialize(json);
return _linkIndex;
}
protected async Task<LinkRegistryEntry> GetLinkIndexEntry(string repository, Cancel ctx)
{
var linkIndex = await FetchLinkIndex(ctx);
if (!linkIndex.Repositories.TryGetValue(repository, out var repositoryLinks))
throw new Exception($"Repository {repository} not found in link index");
return GetNextContentSourceLinkIndexEntry(repositoryLinks, repository);
}
protected static LinkRegistryEntry GetNextContentSourceLinkIndexEntry(IDictionary<string, LinkRegistryEntry> repositoryLinks, string repository)
{
var linkIndexEntry =
(repositoryLinks.TryGetValue("main", out var link)
? link
: repositoryLinks.TryGetValue("master", out link) ? link : null)
?? throw new Exception($"Repository {repository} found in link index, but no main or master branch found");
return linkIndexEntry;
}
protected async Task<LinkReference> Fetch(string repository, string[] keys, Cancel ctx)
{
var linkIndex = await FetchLinkIndex(ctx);
if (!linkIndex.Repositories.TryGetValue(repository, out var repositoryLinks))
throw new Exception($"Repository {repository} not found in link index");
foreach (var key in keys)
{
if (repositoryLinks.TryGetValue(key, out var linkIndexEntry))
return await FetchLinkIndexEntry(repository, linkIndexEntry, ctx);
}
throw new Exception($"Repository found in link index however none of: '{string.Join(", ", keys)}' branches found");
}
protected async Task<LinkReference> FetchLinkIndexEntry(string repository, LinkRegistryEntry linkRegistryEntry, Cancel ctx)
{
var linkReference = await TryGetCachedLinkReference(repository, linkRegistryEntry);
if (linkReference is not null)
return linkReference;
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{linkRegistryEntry.Path}";
_logger.LogInformation("Fetching links.json for '{Repository}': {Url}", repository, url);
var json = await _client.GetStringAsync(url, ctx);
linkReference = Deserialize(json);
WriteLinksJsonCachedFile(repository, linkRegistryEntry, json);
return linkReference;
}
private void WriteLinksJsonCachedFile(string repository, LinkRegistryEntry linkRegistryEntry, string json)
{
var cachedFileName = $"links-elastic-{repository}-{linkRegistryEntry.Branch}-{linkRegistryEntry.ETag}.json";
var cachedPath = Path.Combine(Paths.ApplicationData.FullName, "links", cachedFileName);
if (File.Exists(cachedPath))
return;
try
{
_ = Directory.CreateDirectory(Path.GetDirectoryName(cachedPath)!);
File.WriteAllText(cachedPath, json);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to write cached link reference {CachedPath}", cachedPath);
}
}
private async Task<LinkReference?> TryGetCachedLinkReference(string repository, LinkRegistryEntry linkRegistryEntry)
{
var cachedFileName = $"links-elastic-{repository}-main-{linkRegistryEntry.ETag}.json";
var cachedPath = Path.Combine(Paths.ApplicationData.FullName, "links", cachedFileName);
if (File.Exists(cachedPath))
{
try
{
var json = await File.ReadAllTextAsync(cachedPath);
var linkReference = Deserialize(json);
return linkReference;
}
catch (Exception e)
{
_logger.LogError(e, "Failed to read cached link reference {CachedPath}", cachedPath);
return null;
}
}
return null;
}
public void Dispose()
{
_client.Dispose();
logger.Dispose();
GC.SuppressFinalize(this);
}
}