using Microsoft.AspNetCore.Components.Forms; using Pldpro.Web.UI.Models; using System.Net.Http.Json; namespace Pldpro.Web.UI.Services; public sealed class StorageDocumentClient(IHttpClientFactory factory) : IDocumentClient { private readonly HttpClient _http = factory.CreateClient("AppApi"); private sealed record BucketVm(string Name, DateTime? CreationDate); private sealed record ObjectVm(string Key, long? Size, DateTime? LastModified); public async Task> ListBucketsAsync(CancellationToken ct = default) { var list = await _http.GetFromJsonAsync>("/api/storage/buckets", ct) ?? new(); return list.Select(b => b.Name).ToList(); } public async Task<(IReadOnlyList Items, int Total)> SearchAsync( string bucket, string? query, string? pathPrefix, int page, int pageSize, CancellationToken ct = default) { var objs = await _http.GetFromJsonAsync>($"/api/storage/buckets/{Uri.EscapeDataString(bucket)}/objects", ct) ?? new(); IEnumerable q = objs.Select(o => new DocumentListItem { Bucket = bucket, Key = o.Key, Size = o.Size, LastModified = o.LastModified }); if (!string.IsNullOrWhiteSpace(pathPrefix)) q = q.Where(d => d.Key.StartsWith(pathPrefix!.Trim('/') + "/", StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(query)) { q = q.Where(d => d.FileName.Contains(query, StringComparison.OrdinalIgnoreCase) || d.PathPrefix.Contains(query, StringComparison.OrdinalIgnoreCase)); } var total = q.Count(); var pageItems = q .OrderByDescending(d => d.LastModified ?? DateTime.MinValue) .Skip(page * pageSize) .Take(pageSize) .ToList(); return (pageItems, total); } public async Task GetAsync(string bucket, string key, CancellationToken ct = default) { // Da es keinen Einzel-Endpoint gibt, holen wir die Liste und picken das Objekt. var (items, _) = await SearchAsync(bucket, null, null, 0, int.MaxValue, ct); var d = items.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.Ordinal)); return d is null ? null : new DocumentDetail { Bucket = d.Bucket, Key = d.Key, Size = d.Size, LastModified = d.LastModified, Status = d.Status }; } public async Task UploadAsync(string bucket, string? pathPrefix, IBrowserFile file, long streamLimit, CancellationToken ct = default) { using var stream = file.OpenReadStream(streamLimit); using var content = new MultipartFormDataContent(); content.Add(new StreamContent(stream), "file", file.Name); if (!string.IsNullOrWhiteSpace(pathPrefix)) content.Add(new StringContent(pathPrefix!.Trim('/')), "path"); var resp = await _http.PostAsync($"/api/storage/buckets/{Uri.EscapeDataString(bucket)}/upload", content, ct); resp.EnsureSuccessStatusCode(); } public async Task DeleteAsync(string bucket, string key, CancellationToken ct = default) { var url = $"/api/storage/buckets/{Uri.EscapeDataString(bucket)}/objects/{EncodeKeyForPath(key)}"; var resp = await _http.DeleteAsync(url, ct); resp.EnsureSuccessStatusCode(); } public string GetDownloadUrl(string bucket, string key) => $"/api/storage/buckets/{Uri.EscapeDataString(bucket)}/download/{EncodeKeyForPath(key)}"; // Pfadsegment-weise encodieren: Slashes bleiben Trennzeichen private static string EncodeKeyForPath(string key) => string.Join("/", (key ?? string.Empty) .Split('/', StringSplitOptions.RemoveEmptyEntries) .Select(Uri.EscapeDataString)); }