Sube capa Infrastructure, Solution y archivos de configuracion del Backend
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user