public abstract class CrossLinkFetcher()

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);
	}
}