Files
Finlytic/FinlyticAssets/Services/AssetsIndexService.cs
T

152 lines
6.3 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Models.Assets;
using FinlyticAssets.Util;
using FinlyticCore.Services;
namespace FinlyticAssets.Services;
/// <summary>
/// Provides methods for generating the indexed asset reference file and downloading local asset logos strictly on demand.
/// </summary>
public interface IAssetsIndexService
{
/// <summary>
/// Recreates the index file containing basic asset identifiers (ISIN and Name) for all active, valid assets.
/// </summary>
public Task ReCreateIndexFileAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Strictly On Demand: Downloads and saves the logo SVG for a requested ISIN into the local assets/logos folder.
/// </summary>
public Task<string?> DownloadAndSaveLogoAsync(string isin, CancellationToken cancellationToken = default);
}
/// <summary>
/// Implements the <see cref="IAssetsIndexService"/> to maintain local asset index references and logo file storage.
/// </summary>
public class AssetsIndexService : IAssetsIndexService
{
private readonly IFinlyticLogger<AssetsIndexService> _finlyticLogger;
private readonly IAssetsDbService _assetsDbService;
private static readonly HttpClient _httpClient = new();
private static readonly SemaphoreSlim _fileLock = new(1, 1);
public AssetsIndexService(IFinlyticLogger<AssetsIndexService> finlyticLogger, IAssetsDbService assetsDbService)
{
_finlyticLogger = finlyticLogger;
_assetsDbService = assetsDbService;
}
/// <summary>Inherits documentation from interface.</summary>
public async Task ReCreateIndexFileAsync(CancellationToken cancellationToken = default)
{
try
{
var assets = await _assetsDbService.GetAllValidAssetsAsync();
if (assets == null || !assets.Any())
{
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[AssetsIndexService] No valid assets found in the database to index.");
return;
}
var indexAssets = assets
.DistinctBy(a => a.Isin)
.Select(a => {
string cleanIsin = a.Isin.Trim().ToUpperInvariant();
string imageUrl = $"/api/v1/logo/{cleanIsin}";
return new AssetIndex(cleanIsin, a.Name, imageUrl);
}).ToList();
var directoryPath = Volumes.IndexRelativePath;
var filePath = Path.Combine(directoryPath, "index.json");
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
await _fileLock.WaitAsync(cancellationToken);
try
{
var tempFilePath = Path.Combine(directoryPath, $"index_{Guid.NewGuid():N}.tmp");
using (var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, 4096, useAsync: true))
{
await JsonSerializer.SerializeAsync(fileStream, indexAssets, cancellationToken: cancellationToken);
}
File.Move(tempFilePath, filePath, overwrite: true);
}
finally
{
_fileLock.Release();
}
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsIndexService] Successfully recreated asset index file with {Count} entries pointing to local logos at {Path}",
indexAssets.Count, filePath);
}
catch (IOException ex)
{
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] Concurrent file access while writing asset index file. Skipping cycle.");
}
catch (JsonException ex)
{
await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] Failed to serialize the asset index data to JSON.");
}
catch (Exception ex)
{
await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] An unexpected error occurred while recreating the asset index file.");
}
}
/// <summary>Inherits documentation from interface.</summary>
public async Task<string?> DownloadAndSaveLogoAsync(string isin, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(isin)) return null;
string cleanIsin = isin.Trim().ToUpperInvariant();
string directoryPath = Volumes.LogosRelativePath;
string filePath = Path.Combine(directoryPath, $"{cleanIsin}.svg");
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
if (File.Exists(filePath))
{
return filePath;
}
string targetUrl = $"https://assets.traderepublic.com/img/logos/{cleanIsin}/v2/dark.min.svg";
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, targetUrl);
request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
request.Headers.TryAddWithoutValidation("Accept", "image/svg+xml,image/*,*/*");
request.Headers.TryAddWithoutValidation("Referer", "https://traderepublic.com/");
using var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
byte[] data = await response.Content.ReadAsByteArrayAsync(cancellationToken);
await File.WriteAllBytesAsync(filePath, data, cancellationToken);
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsIndexService] Successfully saved logo SVG for ISIN {Isin} to {Path} on demand", cleanIsin, filePath);
return filePath;
}
}
catch (Exception ex)
{
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] Failed to download logo for ISIN {Isin} from {Url}", cleanIsin, targetUrl);
}
return null;
}
}