Utopia-Canteen-System/Services/EmployeePhotoService.cs

115 lines
3.8 KiB
C#

using System.Collections.Concurrent;
using System.Text;
using MySqlConnector;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Fetches employee photo from hrms.employee_photo by parent_document_id (employee document id from lookup).
/// Handles data URI (data:image/...;base64,...) or raw image bytes. Caches by key to avoid repeated DB calls.
/// </summary>
public class EmployeePhotoService : IEmployeePhotoService
{
private readonly IConfigService _configService;
private readonly ConcurrentDictionary<string, byte[]> _cache = new(StringComparer.OrdinalIgnoreCase);
private const int MaxCacheEntries = 50;
public EmployeePhotoService(IConfigService configService)
{
_configService = configService;
}
/// <inheritdoc />
public async Task<byte[]?> GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(parentDocumentId))
return null;
var key = parentDocumentId.Trim();
if (_cache.TryGetValue(key, out var cached))
return cached;
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return null;
try
{
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
// employee_photo.employee_id stores the employee document id (parent_document_id from employee lookup)
const string sql = "SELECT photo_blob FROM hrms.employee_photo WHERE employee_id = @parentDocumentId LIMIT 1";
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@parentDocumentId", key);
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
return null;
if (reader.IsDBNull(0))
return null;
byte[] blob;
try
{
var len = reader.GetBytes(0, 0, null, 0, 0);
blob = new byte[len];
reader.GetBytes(0, 0, blob, 0, (int)len);
}
catch
{
return null;
}
var imageBytes = DecodeBlobToImageBytes(blob);
if (imageBytes == null || imageBytes.Length == 0)
return null;
// Cache (evict old if needed)
while (_cache.Count >= MaxCacheEntries && _cache.Count > 0)
{
var first = _cache.Keys.FirstOrDefault();
if (first != null)
_cache.TryRemove(first, out _);
else
break;
}
_cache[key] = imageBytes;
return imageBytes;
}
catch
{
return null;
}
}
/// <summary>
/// Converts blob to image bytes: if blob is UTF-8 "data:image...;base64,&lt;payload&gt;", decodes base64; else treats as raw image.
/// </summary>
internal static byte[]? DecodeBlobToImageBytes(byte[] blob)
{
if (blob == null || blob.Length == 0)
return null;
try
{
var str = Encoding.UTF8.GetString(blob);
if (str.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
var commaIndex = str.IndexOf(',');
if (commaIndex < 0)
return null;
var base64 = str.Substring(commaIndex + 1).Trim();
return Convert.FromBase64String(base64);
}
return blob;
}
catch
{
return null;
}
}
}