| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- using System.Text.RegularExpressions;
- namespace bitforum.Services
- {
- public enum UploadTarget
- {
- Editor, // 에디터
- Upload // 직접 첨부
- }
- public enum UploadFolder
- {
- Basic, // 기본 설정
- Document, // 문서
- Faq, // 자주 묻는 질문
- Popup, // 팝업
- Banner, // 배너
- Grade, // 회원 등급
- Template, // 알림 발송 양식
- Member // 회원 사진/아이콘
- }
- public interface IFileUploadService
- {
- // 에디터의 모든 파일 삭제
- public Task CleanUpEditorImagesAsync(string? content);
- // 에디터에 첨부된 이미지를 저장
- public Task<string?> UploadEditorAsync(string? newContent, string? oldContent, UploadFolder folder, int? ID = null);
- // 직접 첨부한 이미지 저장
- public Task<string?> UploadImageAsync(IFormFile? file, UploadFolder folder, int? ID = null);
- // 물리적 파일 저장
- public Task<string?> UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadTarget target, UploadFolder folder, int? ID = null);
- // 파일 삭제
- public bool RemoveFile(string? filePath);
- }
- public class FileUploadService : IFileUploadService
- {
- private readonly ILogger<FileUploadService> _logger;
- private readonly IWebHostEnvironment _environment;
- public FileUploadService(ILogger<FileUploadService> logger, IWebHostEnvironment environment)
- {
- _logger = logger;
- _environment = environment;
- }
- // 절대 경로 생성
- private string GetAbosolutePath(UploadTarget target, UploadFolder folder, int? ID = null)
- {
- return Path.Combine(_environment.WebRootPath, GetRelativePath(target, folder, ID));
- }
- // 상대 경로 생성
- private string GetRelativePath(UploadTarget target, UploadFolder folder, int? ID = null)
- {
- string t = target switch
- {
- UploadTarget.Editor => "editor",
- UploadTarget.Upload => "upload",
- _ => throw new ArgumentException("유효하지 않은 `Upload target` 입니다.")
- };
- string f = folder switch
- {
- UploadFolder.Basic => "basic",
- UploadFolder.Document => "document",
- UploadFolder.Faq => "faq",
- UploadFolder.Popup => "popup",
- UploadFolder.Banner => "banner",
- UploadFolder.Grade => "grade",
- UploadFolder.Template => "template",
- UploadFolder.Member => "member",
- _ => throw new ArgumentException("유효하지 않은 `Upload folder` 입니다.")
- };
- return Path.Combine(t, f, ID?.ToString() ?? string.Empty);
- }
- private async Task CleanUpUnusedImagesAsync(string uploadPath, HashSet<string> usedImages)
- {
- try
- {
- var files = Directory.GetFiles(uploadPath);
- if (files.Length == 0)
- {
- Console.WriteLine("삭제할 미사용 이미지가 없습니다.");
- return;
- }
- var deleteTasks = files.Select(async file =>
- {
- var path = $"/{Path.GetRelativePath(_environment.WebRootPath, file).Replace("\\", "/")}";
- if (!usedImages.Contains(path))
- {
- try
- {
- await Task.Run(() => File.Delete(file)); // 비동기 파일 삭제
- Console.WriteLine($"이미지 삭제: {path}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"이미지 삭제 실패: {path}, 오류: {ex.Message}");
- }
- }
- });
- await Task.WhenAll(deleteTasks); // 모든 파일 삭제를 병렬 실행
- }
- catch (Exception e)
- {
- Console.WriteLine($"미사용 파일 정리 실패: {e.Message}");
- }
- }
- private async Task CleanUpOldImagesAsync(HashSet<string> usedImages, HashSet<string> oldImages)
- {
- try
- {
- var unUsedImages = oldImages.Except(usedImages).ToList(); // 삭제할 이미지 찾기
- if (unUsedImages.Count > 0)
- {
- var deleteTasks = unUsedImages.Select(async img =>
- {
- var path = Path.Combine(_environment.WebRootPath, img.TrimStart('/'));
- if (File.Exists(path))
- {
- try
- {
- await Task.Run(() => File.Delete(path)); // 비동기 삭제
- Console.WriteLine($"이미지 삭제: {path}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"이미지 삭제 실패: {path}, 오류: {ex.Message}");
- }
- }
- });
- await Task.WhenAll(deleteTasks); // 모든 삭제 작업 비동기 실행
- }
- }
- catch (Exception e)
- {
- Console.WriteLine($"미사용 파일 정리 실패: {e.Message}");
- }
- }
- public async Task CleanUpEditorImagesAsync(string? content)
- {
- string? message = null;
- if (string.IsNullOrEmpty(content))
- {
- message = "삭제할 내용이 비어있습니다.";
- Console.WriteLine(message);
- _logger.LogWarning(message);
- return;
- }
- // <img> 태그에서 이미지 URL 목록 가져오기
- var matches = Regex.Matches(content, @"<img[^>]+src=""(?<src>[^""]+)""[^>]*>");
- if (matches.Count == 0)
- {
- message = "삭제할 이미지 파일이 없습니다.";
- Console.WriteLine(message);
- _logger.LogInformation(message);
- return;
- }
- var deleteTasks = matches.Select(async img =>
- {
- string filePath = Path.Combine(_environment.WebRootPath, img.Groups["src"].Value.TrimStart('/'));
- if (File.Exists(filePath))
- {
- try
- {
- await Task.Run(() => File.Delete(filePath)); // 비동기 삭제
- message = $"파일 삭제 완료: {filePath}";
- Console.WriteLine(message);
- _logger.LogInformation(message);
- }
- catch (Exception ex)
- {
- message = $"파일 삭제 실패: {filePath}, 오류: {ex.Message}";
- Console.WriteLine(message);
- _logger.LogError(message);
- }
- }
- });
- await Task.WhenAll(deleteTasks);
- message = $"총 {matches.Count}개의 파일이 삭제됨";
- Console.WriteLine(message);
- _logger.LogInformation(message);
- }
- public async Task<string?> UploadEditorAsync(string? newContent, string? oldContent, UploadFolder folder, int? ID = null)
- {
- string path = GetRelativePath(UploadTarget.Editor, folder, ID).Replace("\\", "/");
- string uploadPath = GetAbosolutePath(UploadTarget.Editor, folder, ID);
- var usedImages = new HashSet<string>(); // 새로운 content에서 사용된 이미지 목록
- var oldImages = new HashSet<string>(); // 기존 content에서 사용된 이미지 목록
- if (!Directory.Exists(uploadPath))
- {
- Directory.CreateDirectory(uploadPath);
- }
- // oldContent에서 기존 이미지 URL 목록
- if (!string.IsNullOrEmpty(oldContent))
- {
- var oldMatches = Regex.Matches(oldContent, @"<img[^>]+src=""(?<src>[^""]+)""[^>]*>");
- foreach (Match img in oldMatches)
- {
- oldImages.Add(img.Groups["src"].Value);
- }
- }
- // <img> 태그에서 현재 사용 중인 이미지 URL 목록 가져오기
- if (!string.IsNullOrEmpty(newContent))
- {
- var matches = Regex.Matches(newContent, @"<img[^>]+src=""(?<src>[^""]+)""[^>]*>");
- foreach (Match img in matches)
- {
- var src = img.Groups["src"].Value;
- if (src.StartsWith("data:image/"))
- {
- try
- {
- var match = Regex.Match(src, @"^data:image/(?<extension>.+?);base64,(?<data>.+)$");
- if (!match.Success)
- {
- throw new Exception("Base64 이미지가 아닙니다.");
- }
- var extension = match.Groups["extension"].Value;
- var data = match.Groups["data"].Value;
- // 확장자와 파일명 생성
- var fileName = $"{Guid.NewGuid()}.{extension}";
- var filePath = Path.Combine(uploadPath, fileName);
- var webPath = $"/{path}/{fileName}";
- await File.WriteAllBytesAsync(filePath, Convert.FromBase64String(data)); // Base64 데이터 디코딩 및 파일 저장
- newContent = newContent.Replace(src, webPath); // HTML에서 Base64 데이터를 실제 이미지 URL로 대체
- usedImages.Add(webPath); // 사용한 이미지 목록에 추가
- }
- catch (Exception e)
- {
- _logger.LogWarning($"파일을 생성할 수 없음: {e.Message}");
- Console.WriteLine(e.Message);
- continue;
- }
- }
- else if (src.StartsWith($"/{path}"))
- {
- usedImages.Add(src);
- }
- }
- }
- // 사용하지 않는 이미지 파일 삭제
- await CleanUpOldImagesAsync(usedImages, oldImages);
- if (ID.HasValue)
- {
- await CleanUpUnusedImagesAsync(uploadPath, usedImages);
- }
- return newContent;
- }
- public async Task<string?> UploadImageAsync(IFormFile? file, UploadFolder folder, int? ID = null)
- {
- return await UploadFileAsync(file, [".jpg", ".jpeg", ".png", ".gif"], UploadTarget.Upload, folder, ID);
- }
- public async Task<string?> UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadTarget target, UploadFolder folder, int ?ID = null)
- {
- if (file == null || file.Length == 0)
- {
- return null;
- }
- var extension = Path.GetExtension(file.FileName).ToLower();
- if (!Array.Exists(allowedExtensions, ext => ext == extension))
- {
- throw new ArgumentException("허용되지 않는 파일 형식입니다.");
- }
- var uploadPath = GetAbosolutePath(target, folder, ID);
- if (!Directory.Exists(uploadPath))
- {
- Directory.CreateDirectory(uploadPath);
- }
- var fileName = $"{Guid.NewGuid()}{extension}";
- var filePath = Path.Combine(uploadPath, fileName);
- var webPath = $"/{GetRelativePath(target, folder, ID)}/{fileName}".Replace("\\", "/");
- // 파일 저장
- using (var stream = new FileStream(filePath, FileMode.Create))
- {
- await file.CopyToAsync(stream);
- }
- return webPath;
- }
- public bool RemoveFile(string? filePath)
- {
- if (string.IsNullOrEmpty(filePath))
- {
- _logger.LogWarning("삭제 요청된 파일 경로가 비어 있습니다.");
- return false;
- }
- var fullPath = Path.Combine(_environment.WebRootPath, filePath.TrimStart('/'));
- if (File.Exists(fullPath))
- {
- try
- {
- File.Delete(fullPath);
- _logger.LogInformation($"파일 삭제 성공: {fullPath}");
- return true;
- }
- catch (Exception e)
- {
- _logger.LogError($"파일 삭제 실패: {fullPath}, 오류: {e.Message}");
- return false;
- }
- }
- _logger.LogWarning($"파일이 존재하지 않음: {fullPath}");
- return false;
- }
- }
- }
|