using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TrueCV.Application.Interfaces; using TrueCV.Infrastructure.Configuration; namespace TrueCV.Infrastructure.Services; public sealed class LocalFileStorageService : IFileStorageService { private readonly string _storagePath; private readonly ILogger _logger; public LocalFileStorageService( IOptions settings, ILogger logger) { _logger = logger; _storagePath = settings.Value.StoragePath; if (!Directory.Exists(_storagePath)) { Directory.CreateDirectory(_storagePath); _logger.LogInformation("Created local storage directory: {Path}", _storagePath); } } public async Task UploadAsync(Stream fileStream, string fileName) { ArgumentNullException.ThrowIfNull(fileStream); ArgumentException.ThrowIfNullOrWhiteSpace(fileName); var extension = Path.GetExtension(fileName); var uniqueFileName = $"{Guid.NewGuid()}{extension}"; var filePath = Path.Combine(_storagePath, uniqueFileName); _logger.LogDebug("Uploading file {FileName} to {FilePath}", fileName, filePath); await using var fileStreamOut = new FileStream(filePath, FileMode.Create, FileAccess.Write); await fileStream.CopyToAsync(fileStreamOut); // Return a file:// URL for local storage var fileUrl = $"file://{filePath}"; _logger.LogInformation("Successfully uploaded file {FileName} to {FileUrl}", fileName, fileUrl); return fileUrl; } public async Task DownloadAsync(string blobUrl) { ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl); var filePath = ExtractFilePathFromUrl(blobUrl); _logger.LogDebug("Downloading file from {FilePath}", filePath); if (!File.Exists(filePath)) { throw new FileNotFoundException($"File not found: {filePath}"); } var memoryStream = new MemoryStream(); await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); await fileStream.CopyToAsync(memoryStream); memoryStream.Position = 0; _logger.LogDebug("Successfully downloaded file from {FilePath}", filePath); return memoryStream; } public Task DeleteAsync(string blobUrl) { ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl); var filePath = ExtractFilePathFromUrl(blobUrl); _logger.LogDebug("Deleting file {FilePath}", filePath); if (File.Exists(filePath)) { File.Delete(filePath); _logger.LogInformation("Successfully deleted file {FilePath}", filePath); } else { _logger.LogWarning("File {FilePath} did not exist when attempting to delete", filePath); } return Task.CompletedTask; } private string ExtractFilePathFromUrl(string fileUrl) { string filePath; if (fileUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { filePath = fileUrl[7..]; } else { filePath = fileUrl; } // Resolve to absolute path and validate it's within storage directory var fullPath = Path.GetFullPath(filePath); var storagePath = Path.GetFullPath(_storagePath); if (!fullPath.StartsWith(storagePath, StringComparison.OrdinalIgnoreCase)) { throw new UnauthorizedAccessException($"Access denied: path is outside storage directory"); } return fullPath; } }