Sube capa Infrastructure, Solution y archivos de configuracion del Backend

This commit is contained in:
FrancoRu
2026-09-18 13:36:47 -03:00
parent 2eb4eeef03
commit 1ef9a0bd03
205 changed files with 77651 additions and 0 deletions
@@ -0,0 +1,217 @@
using Application.Utils.Constants;
using Application.Utils.Constants.Configuration;
using Application.Utils.Helper.SupabaseHelper;
using Microsoft.Extensions.Configuration;
using Supabase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Infrastructure.Storage;
/// <summary>
/// Provides helper methods to interact with Supabase storage, including file upload and public URL generation.
/// </summary>
public class SupabaseHelper : ISupabaseRawStorage
{
/// <summary>
/// Cache-Control max-age, in seconds, applied to uploaded images, set to one year because uploaded files are content-addressed by a fresh GUID name and never need to be invalidated.
/// </summary>
private const string ImageCacheControlMaxAgeSeconds = "31536000";
private readonly Client _client;
private readonly string _bucketName;
private readonly Lazy<Task> _initialization;
public SupabaseHelper(IConfiguration configuration)
{
IConfigurationSection section = configuration.GetSection(ConfigurationKeys.Supabase.Section);
string baseUrl = section[ConfigurationKeys.Supabase.ProjectUrl]!;
string serviceRole = section[ConfigurationKeys.Supabase.ServiceRole]!;
_bucketName = section[ConfigurationKeys.Supabase.BucketName]!;
SupabaseOptions options = new()
{
AutoRefreshToken = true,
AutoConnectRealtime = true
};
_client = new Client(baseUrl, serviceRole, options);
_initialization = new Lazy<Task>(_client.InitializeAsync);
}
/// <summary>
/// Uploads an image stream to Supabase Storage.
/// </summary>
/// <param name="fileStream">The image stream to upload.</param>
/// <param name="fileName">The destination filename in the bucket.</param>
/// <returns>A string representing the new file name.</returns>
/// <exception cref="InvalidOperationException">Thrown when the upload fails.</exception>
public async Task<string> UploadImageAsync<T>(Stream fileStream, string fileName)
{
await _initialization.Value;
try
{
string newFileName = GenerateNameFile(fileName);
string fullPathInBucket = $"{typeof(T).Name}/{newFileName}";
await _client.Storage
.From(_bucketName)
.Upload(UseStreamDotReadMethod(fileStream), fullPathInBucket,
new()
{
CacheControl = ImageCacheControlMaxAgeSeconds,
Upsert = true
}
);
return _client.Storage.From(_bucketName).GetPublicUrl(fullPathInBucket);
}
catch (Exception ex)
{
throw new InvalidOperationException(ErrorMessages.Storage.UploadFailed(ex.Message), ex);
}
}
/// <summary>
/// Deletes an image from the specified storage bucket.
/// </summary>
/// <param name="fileName">The name of the file to delete within the bucket.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when an error occurs while deleting the file.
/// </exception>
public async Task DeleteImageAsync<T>(string fileName)
{
await _initialization.Value;
try
{
await _client.Storage.From(_bucketName).Remove($"{typeof(T).Name}/{fileName}");
}
catch (Exception ex)
{
throw new InvalidOperationException(ErrorMessages.Storage.DeleteFailed(ex.Message), ex);
}
}
/// <summary>
/// Uploads raw content to an arbitrary object path in the bucket, without the per-type folder, cache-control header, or public URL that UploadImageAsync applies.
/// </summary>
/// <param name="objectPath">The exact destination path within the bucket.</param>
/// <param name="content">The content stream to upload.</param>
/// <param name="bucket">Target bucket; null uses the configured SupaBase:BucketName.</param>
/// <exception cref="InvalidOperationException">Thrown when the upload fails.</exception>
public async Task UploadRawAsync(string objectPath, Stream content, string? bucket = null)
{
await _initialization.Value;
try
{
await _client.Storage
.From(bucket ?? _bucketName)
.Upload(UseStreamDotReadMethod(content), objectPath, new() { Upsert = true });
}
catch (Exception ex)
{
throw new InvalidOperationException(ErrorMessages.Storage.UploadFailed(ex.Message), ex);
}
}
/// <summary>
/// Lists raw objects that share a given prefix within the bucket.
/// </summary>
/// <param name="prefix">The bucket-relative path prefix to list.</param>
/// <param name="bucket">Target bucket; null uses the configured SupaBase:BucketName.</param>
/// <exception cref="InvalidOperationException">Thrown when listing fails.</exception>
public async Task<IReadOnlyList<SupabaseStorageEntry>> ListRawAsync(string prefix, string? bucket = null)
{
await _initialization.Value;
try
{
List<Supabase.Storage.FileObject> files = await _client.Storage.From(bucket ?? _bucketName).List(prefix)
?? [];
return files
.Select(file => new SupabaseStorageEntry(
file.Name ?? string.Empty,
file.UpdatedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(file.UpdatedAt.Value, DateTimeKind.Utc))
: null))
.ToList();
}
catch (Exception ex)
{
throw new InvalidOperationException(ErrorMessages.Storage.ListFailed(ex.Message), ex);
}
}
/// <summary>
/// Removes the raw object at the given path within the bucket.
/// </summary>
/// <param name="objectPath">The exact object path within the bucket.</param>
/// <param name="bucket">Target bucket; null uses the configured SupaBase:BucketName.</param>
/// <exception cref="InvalidOperationException">Thrown when the removal fails.</exception>
public async Task RemoveRawAsync(string objectPath, string? bucket = null)
{
await _initialization.Value;
try
{
await _client.Storage.From(bucket ?? _bucketName).Remove(objectPath);
}
catch (Exception ex)
{
throw new InvalidOperationException(ErrorMessages.Storage.RemoveFailed(ex.Message), ex);
}
}
/// <summary>
/// Downloads the raw content of the object at the given path within the bucket.
/// </summary>
/// <param name="objectPath">The exact object path within the bucket.</param>
/// <param name="bucket">Target bucket; null uses the configured SupaBase:BucketName.</param>
/// <exception cref="InvalidOperationException">Thrown when the download fails.</exception>
public async Task<byte[]> DownloadRawAsync(string objectPath, string? bucket = null)
{
await _initialization.Value;
try
{
return await _client.Storage.From(bucket ?? _bucketName).Download(objectPath, (EventHandler<float>?)null);
}
catch (Exception ex)
{
throw new InvalidOperationException(ErrorMessages.Storage.DownloadFailed(ex.Message), ex);
}
}
/// <summary>
/// Reads all bytes from a given stream using the Stream.Read method in chunks and returns the entire content as a byte array.
/// </summary>
/// <param name="stream">The input stream to read from.</param>
/// <returns>A byte array containing all data read from the stream.</returns>
private static byte[] UseStreamDotReadMethod(Stream stream)
{
List<byte> totalStream = [];
byte[] buffer = new byte[32];
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
totalStream.AddRange(buffer[..read]);
}
return [.. totalStream];
}
/// <summary>
/// Generates a new unique file name by replacing the original name with a GUID while preserving the original file extension.
/// </summary>
/// <param name="fileName">The original file name including extension.</param>
/// <returns>A new file name string consisting of a GUID and the original extension.</returns>
private static string GenerateNameFile(string fileName)
{
string extension = Path.GetExtension(fileName);
string newName = Guid.NewGuid().ToString();
return $"{newName}{extension}";
}
}
@@ -0,0 +1,50 @@
using Application.Interfaces.Storage;
using Application.Utils.Constants.Configuration;
using Application.Utils.Helper.SupabaseHelper;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Infrastructure.Storage;
/// <summary>
/// IMedicalRecordStorage implementation that reuses the existing Supabase client through ISupabaseRawStorage, storing every medical record in a dedicated private bucket separate from the shared public-images bucket.
/// </summary>
public sealed class SupabaseMedicalRecordStorage : IMedicalRecordStorage
{
/// <summary>
/// Private bucket, provisioned manually per environment and never created by the client.
/// </summary>
public const string DefaultBucketName = "medical-records";
private readonly ISupabaseRawStorage _rawStorage;
private readonly string _bucketName;
public SupabaseMedicalRecordStorage(ISupabaseRawStorage rawStorage, IConfiguration configuration)
{
_rawStorage = rawStorage;
IConfigurationSection section = configuration.GetSection(ConfigurationKeys.Supabase.Section);
string? configured = section[ConfigurationKeys.Supabase.MedicalRecordsBucketName];
_bucketName = string.IsNullOrWhiteSpace(configured) ? DefaultBucketName : configured;
}
public async Task<string> StoreAsync(
Guid teamId, Guid playerId, string fileName, Stream content, CancellationToken ct = default)
{
string extension = Path.GetExtension(fileName);
string objectPath = $"{teamId}/{playerId}/{Guid.NewGuid()}{extension}";
await _rawStorage.UploadRawAsync(objectPath, content, _bucketName);
return objectPath;
}
public async Task<byte[]> DownloadAsync(string objectPath, CancellationToken ct = default)
{
return await _rawStorage.DownloadRawAsync(objectPath, _bucketName);
}
}