| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.Extensions.Logging;
- using SharedKernel.Storage;
- using System.Text.RegularExpressions;
- namespace Infrastructure.Storage
- {
- public class EditorImageService : IEditorImageService
- {
- private readonly IFileStorage _storage;
- private readonly IWebHostEnvironment _env;
- private readonly ILogger<EditorImageService> _log;
- // src="..."/src='...' 모두 커버 (단순 HTML 정규식)
- private static readonly Regex ImgSrcRegex = new Regex(@"<img\b[^>]*?\bsrc\s*=\s*(?:""(?<src>[^""]+)""|'(?<src>[^']+)')[^>]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- // data:image/png;base64,AAAA...
- private static readonly Regex DataImageRegex = new Regex(@"^data:image/(?<ext>[a-zA-Z0-9.+-]+);base64,(?<data>[a-zA-Z0-9+/=\s]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- public EditorImageService(IFileStorage storage, IWebHostEnvironment env, ILogger<EditorImageService> log)
- {
- _storage = storage;
- _env = env;
- _log = log;
- }
- public async Task<string?> UploadAsync(string? html, FileStoragePath path, CancellationToken ct = default)
- {
- html ??= string.Empty;
- var basePath = path with { Target = UploadTarget.Editor };
- var baseUrlPrefix = "/" + basePath.ToRelativePath(); // "/editors/board/10/3"
- var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
- var rewritten = ImgSrcRegex.Replace(html, match =>
- {
- var src = match.Groups["src"].Value;
- return match.Value;
- });
- foreach (Match m in ImgSrcRegex.Matches(rewritten))
- {
- var src = m.Groups["src"].Value;
- if (src.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase))
- {
- try
- {
- var dm = DataImageRegex.Match(src);
- if (!dm.Success)
- {
- continue;
- }
- var ext = FileUtils.NormalizeExtension(dm.Groups["ext"].Value);
- var bytes = Convert.FromBase64String(dm.Groups["data"].Value);
- var result = await _storage.SaveBytesAsync(bytes, ext, basePath, ct);
- if (result != null)
- {
- rewritten = rewritten.Replace(src, result.Url);
- used.Add(result.Url);
- }
- } catch(Exception e) {
- _log.LogWarning(e, "Failed to persist data:image");
- }
- } else if (FileUtils.IsLocalEditorUrl(src, baseUrlPrefix)) {
- used.Add(src.Replace("\\", "/"));
- }
- }
- /// 미사용 파일 정리: basePath 폴더 내 파일만 대상으로
- CleanupUnusedFiles(basePath, used);
- return rewritten;
- }
- public async Task CleanupByContentAsync(string? html, CancellationToken ct = default)
- {
- if (string.IsNullOrWhiteSpace(html))
- {
- return;
- }
- foreach (Match m in ImgSrcRegex.Matches(html))
- {
- var src = m.Groups["src"].Value;
- if (!FileUtils.IsLocalPublicUrl(src)) {
- continue;
- }
- try {
- _storage.DeleteByUrl(src);
- } catch (Exception e) {
- _log.LogWarning(e, "CleanupByContent delete failed: {0}", src);
- }
- }
- await Task.CompletedTask;
- }
- public void CleanupFolder(FileStoragePath path)
- {
- var basePath = path with { Target = UploadTarget.Editor };
- try
- {
- var dir = GetAbsoluteDirectory(basePath);
- if (!Directory.Exists(dir))
- {
- return;
- }
- Directory.Delete(dir, recursive: true);
- }
- catch (Exception e)
- {
- _log.LogWarning(e, "CleanupFolder failed: {0}", basePath);
- }
- }
- private void CleanupUnusedFiles(FileStoragePath basePath, HashSet<string> usedUrls)
- {
- string dir;
- try {
- dir = GetAbsoluteDirectory(basePath);
- } catch (Exception e) {
- _log.LogWarning(e, "삭제 경로가 존재하지 않습니다.");
- return;
- }
- if (!Directory.Exists(dir))
- {
- return;
- }
- foreach (var file in Directory.GetFiles(dir))
- {
- // file => "/editors/....../filename.ext" 형태 URL로 환산
- var rel = Path.GetRelativePath(_env.WebRootPath, file).Replace("\\", "/");
- var url = "/" + rel;
- if (usedUrls.Contains(url))
- {
- continue;
- }
- try {
- File.Delete(file);
- }
- catch (Exception e) {
- _log.LogWarning(e, "Failed deleting unused file: {File}", file);
- }
- }
- }
- private string GetAbsoluteDirectory(FileStoragePath path)
- {
- var relative = path.ToRelativePath();
- 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;
- }
- }
- }
|