using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using SharedKernel.Storage; namespace Infrastructure.Storage { public sealed class LocalFileStorage : IFileStorage { private readonly IWebHostEnvironment _env; private readonly ILogger _log; public LocalFileStorage(IWebHostEnvironment env, ILogger log) { _env = env; _log = log; } public async Task SaveFileAsync(IFormFile? file, FileStoragePath path, string[] allowedExtensions, CancellationToken ct = default) { if (file is null || file.Length <= 0) { return null; } var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(ext)) { throw new ArgumentException("허용되지 않는 파일 확장자입니다."); } var rel = path.ToRelativePath(); var dir = CombineUnderWebRoot(rel); Directory.CreateDirectory(dir); // 경로 생성(없으면) var fileName = $"{Guid.NewGuid():N}{ext}"; var fullPath = Path.Combine(dir, fileName); await using (var fs = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { await file.CopyToAsync(fs, ct); } var url = "/" + $"{rel}/{fileName}".Replace("\\", "/"); short? width = null, height = null; if (IsImageExtension(ext)) { using var readStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Read); var dim = ImageDimensionHelper.GetDimensions(readStream); if (dim.HasValue) { width = dim.Value.Width; height = dim.Value.Height; } } return new FileUploadResult(url, fileName, file.Length, ext, width, height); } public async Task SaveBytesAsync(ReadOnlyMemory bytes, string extension, FileStoragePath path, CancellationToken ct = default) { if (bytes.Length <= 0) { return null; } var ext = FileUtils.NormalizeExtension(extension); if (ext is not (".jpg" or ".png" or ".gif" or ".bmp" or ".webp")) { throw new ArgumentException("허용되지 않는 파일 확장자입니다."); } var rel = path.ToRelativePath(); var dir = CombineUnderWebRoot(rel); Directory.CreateDirectory(dir); // 경로 생성(없으면) var fileName = $"{Guid.NewGuid():N}{ext}"; var fullPath = Path.Combine(dir, fileName); await File.WriteAllBytesAsync(fullPath, bytes.ToArray(), ct); var url = "/" + $"{rel}/{fileName}".Replace("\\", "/"); short? width = null, height = null; var dim = ImageDimensionHelper.GetDimensions(bytes); if (dim.HasValue) { width = dim.Value.Width; height = dim.Value.Height; } return new FileUploadResult(url, fileName, bytes.Length, ext, width, height); } public void DeleteByUrl(string? url) { if (string.IsNullOrWhiteSpace(url)) { return; } if (!FileUtils.IsLocalPublicUrl(url)) { _log.LogWarning("삭제가 불가한 주소입니다. {0}", url); return; } var fullPath = CombineUnderWebRoot(url.TrimStart('/')); if (!File.Exists(fullPath)) { _log.LogWarning("파일을 찾을 수 없습니다. {0}", fullPath); return; } try { File.Delete(fullPath); _log.LogInformation("파일이 삭제됨 : {0}", fullPath); } catch (Exception e) { _log.LogError(e, "파일 삭제 중 오류가 발생했습니다. {0}", fullPath); } } private static bool IsImageExtension(string ext) { return ext is ".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".bmp"; } private string CombineUnderWebRoot(string relative) { var webRoot = Path.GetFullPath(_env.WebRootPath); var fullPath = Path.GetFullPath(Path.Combine(_env.WebRootPath, relative)); if (!fullPath.StartsWith(webRoot, StringComparison.OrdinalIgnoreCase)) { throw new UnauthorizedAccessException("경로를 찾을 수 없습니다."); } return fullPath; } } }