/*
* Copyright (c) 2024 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System.Security.Cryptography;
using System.Text;
using ProtonVPN.Api.Contracts;
using ProtonVPN.Client.Files.Contracts.Images;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Legacy.OS.Net.Http;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.AppLogs;
using File = System.IO.File;
namespace ProtonVPN.Client.Files.Images;
public class ImageCache : IImageCache
{
private readonly ILogger _logger;
private readonly IConfiguration _config;
private readonly IHttpClient _httpClient;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public ImageCache(ILogger logger,
IConfiguration config,
IFileDownloadHttpClientFactory fileDownloadHttpClientFactory)
{
_logger = logger;
_config = config;
_httpClient = fileDownloadHttpClientFactory.GetHttpClientWithTlsPinning();
}
public async Task GetAsync(string folder, string? downloadUrl)
{
try
{
if (downloadUrl != null)
{
string? localPath = await GetLocalPathOrDownloadAsync(folder, downloadUrl);
if (localPath != null)
{
return new CachedImage() { LocalPath = localPath };
}
}
}
catch (Exception ex)
{
_logger.Error("An error occurred when fetching or downloading an image.", ex);
}
return null;
}
private async Task GetLocalPathOrDownloadAsync(string folder, string downloadUrl)
{
string localPath = GenerateFullFilePath(folder, downloadUrl);
if (await DoesFileExistAsync(localPath))
{
return localPath;
}
return await DownloadAsync(downloadUrl, localPath);
}
private string GenerateFullFilePath(string folder, string downloadUrl)
{
return Path.Combine(_config.ImageCacheFolder, folder, GenerateFileName(downloadUrl));
}
private string GenerateFileName(string downloadUrl)
{
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(downloadUrl));
StringBuilder sb = new();
foreach (byte b in bytes)
{
sb.Append(b.ToString("X2"));
}
return sb.ToString();
}
private async Task DoesFileExistAsync(string localPath)
{
await _semaphore.WaitAsync();
try
{
return File.Exists(localPath);
}
finally
{
_semaphore.Release();
}
}
private async Task DownloadAsync(string downloadUrl, string localPath)
{
await _semaphore.WaitAsync();
try
{
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
return await DownloadImageAsync(downloadUrl, localPath);
}
finally
{
_semaphore.Release();
}
}
private async Task DownloadImageAsync(string downloadUrl, string localPath)
{
try
{
using IHttpResponseMessage response = await _httpClient.GetAsync(downloadUrl);
if (response.IsSuccessStatusCode && response.ContentMediaType.StartsWithIgnoringCase("image/"))
{
using Stream contentStream = await response.Content.ReadAsStreamAsync();
using FileStream fileStream = new(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
await contentStream.CopyToAsync(fileStream);
return localPath;
}
}
catch (Exception ex)
{
_logger.Error($"Failed to download image using URL {downloadUrl}", ex);
}
return null;
}
[Obsolete("Ideally we should remove this method and transform all mappers into Async")]
public CachedImage? Get(string folder, string? downloadUrl)
{
Task task = GetAsync(folder, downloadUrl);
task.Wait();
return task.Result;
}
public IList GetAllFromFolder(string folder)
{
try
{
string? fullPath = Path.Combine(_config.ImageCacheFolder, folder);
return Directory.Exists(fullPath)
? Directory.GetFiles(fullPath).Select(path => new CachedImage() { LocalPath = path }).ToList()
: [];
}
catch (Exception ex)
{
_logger.Error("Failed to fetch all cached image paths.", ex);
}
return [];
}
public void Delete(CachedImage cachedImage)
{
try
{
if (File.Exists(cachedImage.LocalPath))
{
File.Delete(cachedImage.LocalPath);
}
}
catch (Exception ex)
{
_logger.Error($"Failed to delete the cached image '{cachedImage.LocalPath}'.", ex);
}
}
}