namespace TrackQrApi.Features.Assets.Services; public interface IAssetStorageService { Task StoreAsync(Stream stream, string filename, string contentType); Task<(Stream Stream, string ContentType)?> GetAsync(string storageKey); Task DeleteAsync(string storageKey); string GetPublicUrl(string storageKey, HttpContext context); } public class LocalAssetStorageService : IAssetStorageService { private readonly string _basePath; private readonly ILogger _logger; public LocalAssetStorageService(IConfiguration configuration, ILogger logger) { _logger = logger; _basePath = configuration["Storage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "uploads"); // Ensure directory exists if (!Directory.Exists(_basePath)) Directory.CreateDirectory(_basePath); } public async Task StoreAsync(Stream stream, string filename, string contentType) { // Generate unique storage key var extension = Path.GetExtension(filename); var storageKey = $"{Guid.NewGuid()}{extension}"; var filePath = Path.Combine(_basePath, storageKey); await using var fileStream = new FileStream(filePath, FileMode.Create); await stream.CopyToAsync(fileStream); _logger.LogDebug("Stored asset at {FilePath}", filePath); return storageKey; } public Task<(Stream Stream, string ContentType)?> GetAsync(string storageKey) { var filePath = Path.Combine(_basePath, storageKey); if (!File.Exists(filePath)) return Task.FromResult<(Stream, string)?>(null); var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); var contentType = GetContentType(storageKey); return Task.FromResult<(Stream, string)?>((stream, contentType)); } public Task DeleteAsync(string storageKey) { var filePath = Path.Combine(_basePath, storageKey); if (File.Exists(filePath)) { File.Delete(filePath); _logger.LogDebug("Deleted asset at {FilePath}", filePath); } return Task.CompletedTask; } public string GetPublicUrl(string storageKey, HttpContext context) { var baseUrl = $"{context.Request.Scheme}://{context.Request.Host}"; return $"{baseUrl}/assets/{storageKey}"; } private static string GetContentType(string filename) { var extension = Path.GetExtension(filename).ToLowerInvariant(); return extension switch { ".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg", ".gif" => "image/gif", ".webp" => "image/webp", ".svg" => "image/svg+xml", _ => "application/octet-stream" }; } }