Families.Web 1.0.0.0
사진, 동영상, 글, 댓글과 반응을 가족끼리만 공유하는 비공개 앨범·타임라인 서비스입니다.
로딩중...
검색중...
일치하는것 없음
JsonPostStore.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using System.Text.Json;
3using System.Text.Json.Serialization;
5
7
16public sealed class JsonPostStore : IPostStore
17{
26 private readonly IFamilyTenantStore _tenants;
35 private static readonly SemaphoreSlim _gate = new(1, 1);
36
45 private static readonly JsonSerializerOptions _jsonOpts = new()
46 {
47 WriteIndented = true,
48 PropertyNameCaseInsensitive = true,
49 Converters = { new JsonStringEnumConverter() }
50 };
51
68 public JsonPostStore(IFamilyTenantStore tenants) => _tenants = tenants;
69
94 private string PostsDir(string slug) =>
95 Path.Combine(_tenants.GetTenantDataPath(slug), "posts");
96
129 private string PostPath(string slug, string postId) =>
130 Path.Combine(PostsDir(slug), $"{Sanitize(postId)}.json");
131
172 public async Task<PostEntry?> GetAsync(string slug, string postId, CancellationToken ct = default)
173 {
174 var path = PostPath(slug, postId);
175 if (!File.Exists(path)) return null;
176
177 await _gate.WaitAsync(ct).ConfigureAwait(false);
178 try
179 {
180 await using var fs = File.OpenRead(path);
181 return await JsonSerializer.DeserializeAsync<PostEntry>(fs, _jsonOpts, ct).ConfigureAwait(false);
182 }
183 finally { _gate.Release(); }
184 }
185
218 public async Task<IReadOnlyList<PostEntry>> GetAllAsync(string slug, CancellationToken ct = default)
219 {
220 var dir = PostsDir(slug);
221 if (!Directory.Exists(dir)) return [];
222
223 var list = new List<PostEntry>();
224 foreach (var file in Directory.GetFiles(dir, "*.json"))
225 {
226 await _gate.WaitAsync(ct).ConfigureAwait(false);
227 try
228 {
229 await using var fs = File.OpenRead(file);
230 var p = await JsonSerializer.DeserializeAsync<PostEntry>(fs, _jsonOpts, ct).ConfigureAwait(false);
231 if (p != null) list.Add(p);
232 }
233 catch { /* 깨진 파일 하나로 전체 목록이 죽지 않도록 */ }
234 finally { _gate.Release(); }
235 }
236 return list.OrderByDescending(p => p.IsPinned).ThenByDescending(p => p.PostedAt).ToList();
237 }
238
287 public async Task<(IReadOnlyList<PostEntry> Items, int TotalCount)> GetPageAsync(
288 string slug, int page, int pageSize, CancellationToken ct = default)
289 {
290 var all = await GetAllAsync(slug, ct).ConfigureAwait(false);
291 var items = all.Skip((page - 1) * pageSize).Take(pageSize).ToList();
292 return (items, all.Count);
293 }
294
335 public async Task<IReadOnlyList<PostEntry>> GetByAlbumAsync(string slug, string albumId, CancellationToken ct = default)
336 {
337 var all = await GetAllAsync(slug, ct).ConfigureAwait(false);
338 return all.Where(p => p.AlbumId == albumId).ToList();
339 }
340
397 public async Task<(IReadOnlyList<PostEntry> Items, int TotalCount)> GetByAlbumPageAsync(
398 string slug, string albumId, int page, int pageSize, CancellationToken ct = default)
399 {
400 var filtered = await GetByAlbumAsync(slug, albumId, ct).ConfigureAwait(false);
401 var items = filtered.Skip((page - 1) * pageSize).Take(pageSize).ToList();
402 return (items, filtered.Count);
403 }
404
445 public async Task SaveAsync(string slug, PostEntry post, CancellationToken ct = default)
446 {
447 var dir = PostsDir(slug);
448 Directory.CreateDirectory(dir);
449 Directory.CreateDirectory(Path.Combine(_tenants.GetTenantDataPath(slug), "media", post.Id));
450
451 var path = PostPath(slug, post.Id);
452 var tmp = path + ".tmp";
453
454 await _gate.WaitAsync(ct).ConfigureAwait(false);
455 try
456 {
457 await using (var fs = File.Create(tmp))
458 await JsonSerializer.SerializeAsync(fs, post, _jsonOpts, ct).ConfigureAwait(false);
459
460 File.Copy(tmp, path, overwrite: true);
461 File.Delete(tmp);
462 }
463 finally { _gate.Release(); }
464 }
465
506 public Task DeleteAsync(string slug, string postId, CancellationToken ct = default)
507 {
508 var path = PostPath(slug, postId);
509 if (File.Exists(path)) File.Delete(path);
510
511 var mediaDir = Path.Combine(_tenants.GetTenantDataPath(slug), "media", postId);
512 if (Directory.Exists(mediaDir)) Directory.Delete(mediaDir, recursive: true);
513
514 return Task.CompletedTask;
515 }
516
541 private static string Sanitize(string id) =>
542 string.Concat(id.Split(Path.GetInvalidFileNameChars()));
543}
async Task< PostEntry?> GetAsync(string slug, string postId, CancellationToken ct=default)
async Task SaveAsync(string slug, PostEntry post, CancellationToken ct=default)
static string Sanitize(string id)
readonly IFamilyTenantStore _tenants
JsonPostStore(IFamilyTenantStore tenants)
async Task< IReadOnlyList< PostEntry > > GetByAlbumAsync(string slug, string albumId, CancellationToken ct=default)
async Task<(IReadOnlyList< PostEntry > Items, int TotalCount)> GetPageAsync(string slug, int page, int pageSize, CancellationToken ct=default)
Task DeleteAsync(string slug, string postId, CancellationToken ct=default)
static readonly JsonSerializerOptions _jsonOpts
string PostPath(string slug, string postId)
static readonly SemaphoreSlim _gate
async Task<(IReadOnlyList< PostEntry > Items, int TotalCount)> GetByAlbumPageAsync(string slug, string albumId, int page, int pageSize, CancellationToken ct=default)
async Task< IReadOnlyList< PostEntry > > GetAllAsync(string slug, CancellationToken ct=default)