FileUploadService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. using System.Text.RegularExpressions;
  2. namespace bitforum.Services
  3. {
  4. public enum UploadTarget
  5. {
  6. Editor, // 에디터
  7. Upload // 직접 첨부
  8. }
  9. public enum UploadFolder
  10. {
  11. Basic, // 기본 설정
  12. Document, // 문서
  13. Faq, // 자주 묻는 질문
  14. Popup, // 팝업
  15. Banner, // 배너
  16. Grade, // 회원 등급
  17. Template, // 알림 발송 양식
  18. Member // 회원 사진/아이콘
  19. }
  20. public interface IFileUploadService
  21. {
  22. // 에디터의 모든 파일 삭제
  23. public Task CleanUpEditorImagesAsync(string? content);
  24. // 에디터에 첨부된 이미지를 저장
  25. public Task<string?> UploadEditorAsync(string? newContent, string? oldContent, UploadFolder folder, int? ID = null);
  26. // 직접 첨부한 이미지 저장
  27. public Task<string?> UploadImageAsync(IFormFile? file, UploadFolder folder, int? ID = null);
  28. // 물리적 파일 저장
  29. public Task<string?> UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadTarget target, UploadFolder folder, int? ID = null);
  30. // 파일 삭제
  31. public bool RemoveFile(string? filePath);
  32. }
  33. public class FileUploadService : IFileUploadService
  34. {
  35. private readonly ILogger<FileUploadService> _logger;
  36. private readonly IWebHostEnvironment _environment;
  37. public FileUploadService(ILogger<FileUploadService> logger, IWebHostEnvironment environment)
  38. {
  39. _logger = logger;
  40. _environment = environment;
  41. }
  42. // 절대 경로 생성
  43. private string GetAbosolutePath(UploadTarget target, UploadFolder folder, int? ID = null)
  44. {
  45. return Path.Combine(_environment.WebRootPath, GetRelativePath(target, folder, ID));
  46. }
  47. // 상대 경로 생성
  48. private string GetRelativePath(UploadTarget target, UploadFolder folder, int? ID = null)
  49. {
  50. string t = target switch
  51. {
  52. UploadTarget.Editor => "editor",
  53. UploadTarget.Upload => "upload",
  54. _ => throw new ArgumentException("유효하지 않은 `Upload target` 입니다.")
  55. };
  56. string f = folder switch
  57. {
  58. UploadFolder.Basic => "basic",
  59. UploadFolder.Document => "document",
  60. UploadFolder.Faq => "faq",
  61. UploadFolder.Popup => "popup",
  62. UploadFolder.Banner => "banner",
  63. UploadFolder.Grade => "grade",
  64. UploadFolder.Template => "template",
  65. UploadFolder.Member => "member",
  66. _ => throw new ArgumentException("유효하지 않은 `Upload folder` 입니다.")
  67. };
  68. return Path.Combine(t, f, ID?.ToString() ?? string.Empty);
  69. }
  70. private async Task CleanUpUnusedImagesAsync(string uploadPath, HashSet<string> usedImages)
  71. {
  72. try
  73. {
  74. var files = Directory.GetFiles(uploadPath);
  75. if (files.Length == 0)
  76. {
  77. Console.WriteLine("삭제할 미사용 이미지가 없습니다.");
  78. return;
  79. }
  80. var deleteTasks = files.Select(async file =>
  81. {
  82. var path = $"/{Path.GetRelativePath(_environment.WebRootPath, file).Replace("\\", "/")}";
  83. if (!usedImages.Contains(path))
  84. {
  85. try
  86. {
  87. await Task.Run(() => File.Delete(file)); // 비동기 파일 삭제
  88. Console.WriteLine($"이미지 삭제: {path}");
  89. }
  90. catch (Exception ex)
  91. {
  92. Console.WriteLine($"이미지 삭제 실패: {path}, 오류: {ex.Message}");
  93. }
  94. }
  95. });
  96. await Task.WhenAll(deleteTasks); // 모든 파일 삭제를 병렬 실행
  97. }
  98. catch (Exception e)
  99. {
  100. Console.WriteLine($"미사용 파일 정리 실패: {e.Message}");
  101. }
  102. }
  103. private async Task CleanUpOldImagesAsync(HashSet<string> usedImages, HashSet<string> oldImages)
  104. {
  105. try
  106. {
  107. var unUsedImages = oldImages.Except(usedImages).ToList(); // 삭제할 이미지 찾기
  108. if (unUsedImages.Count > 0)
  109. {
  110. var deleteTasks = unUsedImages.Select(async img =>
  111. {
  112. var path = Path.Combine(_environment.WebRootPath, img.TrimStart('/'));
  113. if (File.Exists(path))
  114. {
  115. try
  116. {
  117. await Task.Run(() => File.Delete(path)); // 비동기 삭제
  118. Console.WriteLine($"이미지 삭제: {path}");
  119. }
  120. catch (Exception ex)
  121. {
  122. Console.WriteLine($"이미지 삭제 실패: {path}, 오류: {ex.Message}");
  123. }
  124. }
  125. });
  126. await Task.WhenAll(deleteTasks); // 모든 삭제 작업 비동기 실행
  127. }
  128. }
  129. catch (Exception e)
  130. {
  131. Console.WriteLine($"미사용 파일 정리 실패: {e.Message}");
  132. }
  133. }
  134. public async Task CleanUpEditorImagesAsync(string? content)
  135. {
  136. string? message = null;
  137. if (string.IsNullOrEmpty(content))
  138. {
  139. message = "삭제할 내용이 비어있습니다.";
  140. Console.WriteLine(message);
  141. _logger.LogWarning(message);
  142. return;
  143. }
  144. // <img> 태그에서 이미지 URL 목록 가져오기
  145. var matches = Regex.Matches(content, @"<img[^>]+src=""(?<src>[^""]+)""[^>]*>");
  146. if (matches.Count == 0)
  147. {
  148. message = "삭제할 이미지 파일이 없습니다.";
  149. Console.WriteLine(message);
  150. _logger.LogInformation(message);
  151. return;
  152. }
  153. var deleteTasks = matches.Select(async img =>
  154. {
  155. string filePath = Path.Combine(_environment.WebRootPath, img.Groups["src"].Value.TrimStart('/'));
  156. if (File.Exists(filePath))
  157. {
  158. try
  159. {
  160. await Task.Run(() => File.Delete(filePath)); // 비동기 삭제
  161. message = $"파일 삭제 완료: {filePath}";
  162. Console.WriteLine(message);
  163. _logger.LogInformation(message);
  164. }
  165. catch (Exception ex)
  166. {
  167. message = $"파일 삭제 실패: {filePath}, 오류: {ex.Message}";
  168. Console.WriteLine(message);
  169. _logger.LogError(message);
  170. }
  171. }
  172. });
  173. await Task.WhenAll(deleteTasks);
  174. message = $"총 {matches.Count}개의 파일이 삭제됨";
  175. Console.WriteLine(message);
  176. _logger.LogInformation(message);
  177. }
  178. public async Task<string?> UploadEditorAsync(string? newContent, string? oldContent, UploadFolder folder, int? ID = null)
  179. {
  180. string path = GetRelativePath(UploadTarget.Editor, folder, ID).Replace("\\", "/");
  181. string uploadPath = GetAbosolutePath(UploadTarget.Editor, folder, ID);
  182. var usedImages = new HashSet<string>(); // 새로운 content에서 사용된 이미지 목록
  183. var oldImages = new HashSet<string>(); // 기존 content에서 사용된 이미지 목록
  184. if (!Directory.Exists(uploadPath))
  185. {
  186. Directory.CreateDirectory(uploadPath);
  187. }
  188. // oldContent에서 기존 이미지 URL 목록
  189. if (!string.IsNullOrEmpty(oldContent))
  190. {
  191. var oldMatches = Regex.Matches(oldContent, @"<img[^>]+src=""(?<src>[^""]+)""[^>]*>");
  192. foreach (Match img in oldMatches)
  193. {
  194. oldImages.Add(img.Groups["src"].Value);
  195. }
  196. }
  197. // <img> 태그에서 현재 사용 중인 이미지 URL 목록 가져오기
  198. if (!string.IsNullOrEmpty(newContent))
  199. {
  200. var matches = Regex.Matches(newContent, @"<img[^>]+src=""(?<src>[^""]+)""[^>]*>");
  201. foreach (Match img in matches)
  202. {
  203. var src = img.Groups["src"].Value;
  204. if (src.StartsWith("data:image/"))
  205. {
  206. try
  207. {
  208. var match = Regex.Match(src, @"^data:image/(?<extension>.+?);base64,(?<data>.+)$");
  209. if (!match.Success)
  210. {
  211. throw new Exception("Base64 이미지가 아닙니다.");
  212. }
  213. var extension = match.Groups["extension"].Value;
  214. var data = match.Groups["data"].Value;
  215. // 확장자와 파일명 생성
  216. var fileName = $"{Guid.NewGuid()}.{extension}";
  217. var filePath = Path.Combine(uploadPath, fileName);
  218. var webPath = $"/{path}/{fileName}";
  219. await File.WriteAllBytesAsync(filePath, Convert.FromBase64String(data)); // Base64 데이터 디코딩 및 파일 저장
  220. newContent = newContent.Replace(src, webPath); // HTML에서 Base64 데이터를 실제 이미지 URL로 대체
  221. usedImages.Add(webPath); // 사용한 이미지 목록에 추가
  222. }
  223. catch (Exception e)
  224. {
  225. _logger.LogWarning($"파일을 생성할 수 없음: {e.Message}");
  226. Console.WriteLine(e.Message);
  227. continue;
  228. }
  229. }
  230. else if (src.StartsWith($"/{path}"))
  231. {
  232. usedImages.Add(src);
  233. }
  234. }
  235. }
  236. // 사용하지 않는 이미지 파일 삭제
  237. await CleanUpOldImagesAsync(usedImages, oldImages);
  238. if (ID.HasValue)
  239. {
  240. await CleanUpUnusedImagesAsync(uploadPath, usedImages);
  241. }
  242. return newContent;
  243. }
  244. public async Task<string?> UploadImageAsync(IFormFile? file, UploadFolder folder, int? ID = null)
  245. {
  246. return await UploadFileAsync(file, [".jpg", ".jpeg", ".png", ".gif"], UploadTarget.Upload, folder, ID);
  247. }
  248. public async Task<string?> UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadTarget target, UploadFolder folder, int ?ID = null)
  249. {
  250. if (file == null || file.Length == 0)
  251. {
  252. return null;
  253. }
  254. var extension = Path.GetExtension(file.FileName).ToLower();
  255. if (!Array.Exists(allowedExtensions, ext => ext == extension))
  256. {
  257. throw new ArgumentException("허용되지 않는 파일 형식입니다.");
  258. }
  259. var uploadPath = GetAbosolutePath(target, folder, ID);
  260. if (!Directory.Exists(uploadPath))
  261. {
  262. Directory.CreateDirectory(uploadPath);
  263. }
  264. var fileName = $"{Guid.NewGuid()}{extension}";
  265. var filePath = Path.Combine(uploadPath, fileName);
  266. var webPath = $"/{GetRelativePath(target, folder, ID)}/{fileName}".Replace("\\", "/");
  267. // 파일 저장
  268. using (var stream = new FileStream(filePath, FileMode.Create))
  269. {
  270. await file.CopyToAsync(stream);
  271. }
  272. return webPath;
  273. }
  274. public bool RemoveFile(string? filePath)
  275. {
  276. if (string.IsNullOrEmpty(filePath))
  277. {
  278. _logger.LogWarning("삭제 요청된 파일 경로가 비어 있습니다.");
  279. return false;
  280. }
  281. var fullPath = Path.Combine(_environment.WebRootPath, filePath.TrimStart('/'));
  282. if (File.Exists(fullPath))
  283. {
  284. try
  285. {
  286. File.Delete(fullPath);
  287. _logger.LogInformation($"파일 삭제 성공: {fullPath}");
  288. return true;
  289. }
  290. catch (Exception e)
  291. {
  292. _logger.LogError($"파일 삭제 실패: {fullPath}, 오류: {e.Message}");
  293. return false;
  294. }
  295. }
  296. _logger.LogWarning($"파일이 존재하지 않음: {fullPath}");
  297. return false;
  298. }
  299. }
  300. }