Families.Web 1.0.0.0
사진, 동영상, 글, 댓글과 반응을 가족끼리만 공유하는 비공개 앨범·타임라인 서비스입니다.
로딩중...
검색중...
일치하는것 없음
LocalMediaService.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
3using Microsoft.AspNetCore.Components.Forms;
4
6
15public sealed class LocalMediaService : IMediaService
16{
25 private readonly IFamilyTenantStore _tenants;
35
44 private static readonly string[] AllowedImageExts = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
53 private static readonly string[] AllowedVideoExts = [".mp4", ".webm", ".mov", ".m4v"];
54
80 {
81 _tenants = tenants;
82 _globalSettings = globalSettings;
83 }
84
109 private static long ToBytes(int mb) => mb <= 0 ? long.MaxValue : mb * 1024L * 1024L;
110
143 private async Task<(long ImageBytes, long VideoBytes)> GetLimitsAsync(string slug, CancellationToken ct)
144 {
145 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false);
146 var settings = await _globalSettings.GetAsync(ct).ConfigureAwait(false);
147
148 var imageMb = config?.MaxImageSizeMb ?? settings.MaxImageSizeMb;
149 var videoMb = config?.MaxVideoSizeMb ?? settings.MaxVideoSizeMb;
150 return (ToBytes(imageMb), ToBytes(videoMb));
151 }
152
209 public async Task<string> UploadPostMediaAsync(string slug, string postId, IBrowserFile file, CancellationToken ct = default)
210 {
211 var ext = Path.GetExtension(file.Name).ToLowerInvariant();
212 bool isVideo = Array.Exists(AllowedVideoExts, e => e == ext);
213 bool isImage = Array.Exists(AllowedImageExts, e => e == ext);
214
215 if (!isVideo && !isImage)
216 throw new InvalidOperationException($"허용되지 않는 파일 형식입니다: {ext}");
217
218 var (imageLimit, videoLimit) = await GetLimitsAsync(slug, ct).ConfigureAwait(false);
219 var limit = isVideo ? videoLimit : imageLimit;
220 if (file.Size > limit)
221 throw new InvalidOperationException(isVideo
222 ? $"동영상은 {FormatLimit(videoLimit)} 이하여야 합니다."
223 : $"이미지는 {FormatLimit(imageLimit)} 이하여야 합니다.");
224
225 var mediaDir = Path.Combine(_tenants.GetTenantDataPath(slug), "media", postId);
226 Directory.CreateDirectory(mediaDir);
227
228 var fileName = $"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{ext}";
229 var destPath = Path.Combine(mediaDir, fileName);
230
231 await using var dest = File.Create(destPath);
232 await file.OpenReadStream(limit, ct).CopyToAsync(dest, ct).ConfigureAwait(false);
233
234 return fileName;
235 }
236
285 public async Task<string> UploadCoverAsync(string slug, IBrowserFile file, CancellationToken ct = default)
286 {
287 var ext = Path.GetExtension(file.Name).ToLowerInvariant();
288 if (!Array.Exists(AllowedImageExts, e => e == ext))
289 throw new InvalidOperationException($"허용되지 않는 이미지 형식입니다: {ext}");
290
291 var (imageLimit, _) = await GetLimitsAsync(slug, ct).ConfigureAwait(false);
292 if (file.Size > imageLimit)
293 throw new InvalidOperationException($"커버 이미지는 {FormatLimit(imageLimit)} 이하여야 합니다.");
294
295 var dir = _tenants.GetTenantDataPath(slug);
296 Directory.CreateDirectory(dir);
297
298 var fileName = $"cover{ext}";
299 await using var dest = File.Create(Path.Combine(dir, fileName));
300 await file.OpenReadStream(imageLimit, ct).CopyToAsync(dest, ct).ConfigureAwait(false);
301
302 var config = await _tenants.GetAsync(slug, ct).ConfigureAwait(false) ?? new Models.FamilyConfig { Slug = slug };
303 config.CoverImageFileName = fileName;
304 await _tenants.SaveAsync(config, ct).ConfigureAwait(false);
305
306 return fileName;
307 }
308
333 private static string FormatLimit(long bytes) =>
334 bytes == long.MaxValue ? "무제한" : $"{bytes / (1024 * 1024)}MB";
335
384 public Task DeletePostMediaAsync(string slug, string postId, string fileName, CancellationToken ct = default)
385 {
386 var path = Path.Combine(_tenants.GetTenantDataPath(slug), "media", postId, fileName);
387 if (File.Exists(path)) File.Delete(path);
388 return Task.CompletedTask;
389 }
390
431 public Task<IReadOnlyList<MediaInfo>> GetPostMediaAsync(string slug, string postId, CancellationToken ct = default)
432 {
433 var dir = Path.Combine(_tenants.GetTenantDataPath(slug), "media", postId);
434 if (!Directory.Exists(dir)) return Task.FromResult<IReadOnlyList<MediaInfo>>([]);
435
436 var list = Directory.GetFiles(dir)
437 .Select(f =>
438 {
439 var fi = new FileInfo(f);
440 return new MediaInfo
441 {
442 FileName = fi.Name,
443 Url = GetMediaUrl(slug, postId, fi.Name),
444 ThumbUrl = GetThumbUrl(slug, postId, fi.Name),
445 SizeBytes = fi.Length,
446 LastModified = fi.LastWriteTime
447 };
448 })
449 .ToList();
450
451 return Task.FromResult<IReadOnlyList<MediaInfo>>(list);
452 }
453
494 public string GetMediaUrl(string slug, string postId, string fileName) =>
495 $"/family-data/{slug}/media/{postId}/{Uri.EscapeDataString(fileName)}";
496
537 public string GetThumbUrl(string slug, string postId, string fileName) =>
538 GetMediaUrl(slug, postId, fileName);
539
572 public string GetCoverUrl(string slug, string fileName) =>
573 $"/family-data/{slug}/{Uri.EscapeDataString(fileName)}";
574}
Task DeletePostMediaAsync(string slug, string postId, string fileName, CancellationToken ct=default)
async Task<(long ImageBytes, long VideoBytes)> GetLimitsAsync(string slug, CancellationToken ct)
LocalMediaService(IFamilyTenantStore tenants, IGlobalSettingsStore globalSettings)
readonly IFamilyTenantStore _tenants
string GetCoverUrl(string slug, string fileName)
string GetThumbUrl(string slug, string postId, string fileName)
Task< IReadOnlyList< MediaInfo > > GetPostMediaAsync(string slug, string postId, CancellationToken ct=default)
async Task< string > UploadCoverAsync(string slug, IBrowserFile file, CancellationToken ct=default)
readonly IGlobalSettingsStore _globalSettings
static readonly string[] AllowedImageExts
string GetMediaUrl(string slug, string postId, string fileName)
async Task< string > UploadPostMediaAsync(string slug, string postId, IBrowserFile file, CancellationToken ct=default)
static readonly string[] AllowedVideoExts