WeddingPlatform.Web 1.0.0.0
지도, 갤러리, 방명록, 계좌 안내와 배경음악을 링크 하나에 담는 무료 모바일 청첩장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
JsonTenantStore.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using System.Text.Json;
3using System.Text.Json.Serialization;
4using Wedding.Common;
6
8
17public sealed class JsonTenantStore : ITenantStore
18{
27 private readonly string _dataRoot;
36 private static readonly SemaphoreSlim _gate = new(1, 1);
37
46 private static readonly JsonSerializerOptions _jsonOpts = new()
47 {
48 WriteIndented = true,
49 PropertyNameCaseInsensitive = true,
50 Converters = { new WeddingLayoutModeJsonConverter(), new JsonStringEnumConverter() }
51 };
52
70 {
72 Directory.CreateDirectory(_dataRoot);
73 }
74
99 public string GetTenantDataPath(string slug) =>
100 Path.Combine(_dataRoot, Sanitize(slug));
101
134 public async Task<TenantConfig?> GetAsync(string slug, CancellationToken ct = default)
135 {
136 var path = ConfigPath(slug);
137 if (!File.Exists(path)) return null;
138
139 await _gate.WaitAsync(ct).ConfigureAwait(false);
140 try
141 {
142 return await ReadTenantAsync(path, ct).ConfigureAwait(false);
143 }
144 finally { _gate.Release(); }
145 }
146
171 public async Task<IReadOnlyList<TenantConfig>> GetAllAsync(CancellationToken ct = default)
172 {
173 var list = new List<TenantConfig>();
174 if (!Directory.Exists(_dataRoot)) return list;
175
176 foreach (var dir in Directory.GetDirectories(_dataRoot))
177 {
178 var cfg = Path.Combine(dir, "config.json");
179 if (!File.Exists(cfg)) continue;
180
181 await _gate.WaitAsync(ct).ConfigureAwait(false);
182 try
183 {
184 var t = await ReadTenantAsync(cfg, ct).ConfigureAwait(false);
185 if (t != null) list.Add(t);
186 }
187 catch (JsonException ex)
188 {
189 LogTenantReadError(cfg, ex);
190 }
191 finally { _gate.Release(); }
192 }
193 return list;
194 }
195
228 public async Task SaveAsync(TenantConfig config, CancellationToken ct = default)
229 {
230 var dir = GetTenantDataPath(config.Slug);
231 Directory.CreateDirectory(dir);
232 Directory.CreateDirectory(Path.Combine(dir, "gallery"));
233 Directory.CreateDirectory(Path.Combine(dir, "thumb"));
234
235 var tmp = ConfigPath(config.Slug) + ".tmp";
236 await _gate.WaitAsync(ct).ConfigureAwait(false);
237 try
238 {
239 await using (var fs = File.Create(tmp))
240 await JsonSerializer.SerializeAsync(fs, config, _jsonOpts, ct).ConfigureAwait(false);
241
242 File.Copy(tmp, ConfigPath(config.Slug), overwrite: true);
243 File.Delete(tmp);
244 }
245 finally { _gate.Release(); }
246 }
247
280 public Task<bool> ExistsAsync(string slug, CancellationToken ct = default) =>
281 Task.FromResult(File.Exists(ConfigPath(slug)));
282
315 public Task DeleteAsync(string slug, CancellationToken ct = default)
316 {
317 var dir = GetTenantDataPath(slug);
318 if (Directory.Exists(dir))
319 Directory.Delete(dir, recursive: true);
320 return Task.CompletedTask;
321 }
322
347 private string ConfigPath(string slug) =>
348 Path.Combine(GetTenantDataPath(slug), "config.json");
349
374 private static string Sanitize(string slug) =>
375 string.Concat(slug.ToLowerInvariant().Split(Path.GetInvalidFileNameChars()));
376
409 private static async Task<TenantConfig?> ReadTenantAsync(string path, CancellationToken ct)
410 {
411 var json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false);
412 LogLegacyLayoutModeFallback(path, json);
413 return JsonSerializer.Deserialize<TenantConfig>(json, _jsonOpts);
414 }
415
440 private static void LogLegacyLayoutModeFallback(string path, string json)
441 {
442 var raw = TryGetDesignLayoutMode(json);
443 if (raw is null || IsCanonicalLayoutMode(raw)) return;
444
445 Console.Error.WriteLine($"[JsonTenantStore] Legacy DesignSettings.LayoutMode '{raw}' in '{path}' was loaded with compatibility fallback.");
446 }
447
472 private static string? TryGetDesignLayoutMode(string json)
473 {
474 try
475 {
476 using var doc = JsonDocument.Parse(json);
477 if (!TryGetProperty(doc.RootElement, "DesignSettings", out var design)) return null;
478 if (!TryGetProperty(design, "LayoutMode", out var layout)) return null;
479
480 return layout.ValueKind switch
481 {
482 JsonValueKind.String => layout.GetString(),
483 JsonValueKind.Null => "",
484 JsonValueKind.Number => layout.GetRawText(),
485 _ => layout.GetRawText(),
486 };
487 }
488 catch (JsonException)
489 {
490 return null;
491 }
492 }
493
534 private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
535 {
536 foreach (var property in element.EnumerateObject())
537 {
538 if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
539 {
540 value = property.Value;
541 return true;
542 }
543 }
544
545 value = default;
546 return false;
547 }
548
573 private static bool IsCanonicalLayoutMode(string? value) =>
574 value is not null
575 && (string.Equals(value, "WebPage", StringComparison.Ordinal)
576 || string.Equals(value, "TabMenu", StringComparison.Ordinal)
577 || string.Equals(value, "Gallery", StringComparison.Ordinal)
578 || string.Equals(value, "Story", StringComparison.Ordinal)
579 || string.Equals(value, "Card", StringComparison.Ordinal)
580 || string.Equals(value, "PhotoBook", StringComparison.Ordinal));
581
606 private static void LogTenantReadError(string path, JsonException ex)
607 {
608 Console.Error.WriteLine($"[JsonTenantStore] Skipped invalid tenant JSON '{path}': {ex.Message}");
609 }
610}
static readonly SemaphoreSlim _gate
static ? string TryGetDesignLayoutMode(string json)
static async Task< TenantConfig?> ReadTenantAsync(string path, CancellationToken ct)
static bool IsCanonicalLayoutMode(string? value)
async Task< IReadOnlyList< TenantConfig > > GetAllAsync(CancellationToken ct=default)
static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
async Task SaveAsync(TenantConfig config, CancellationToken ct=default)
async Task< TenantConfig?> GetAsync(string slug, CancellationToken ct=default)
static readonly JsonSerializerOptions _jsonOpts
Task< bool > ExistsAsync(string slug, CancellationToken ct=default)
static void LogLegacyLayoutModeFallback(string path, string json)
static void LogTenantReadError(string path, JsonException ex)
Task DeleteAsync(string slug, CancellationToken ct=default)