using Amazon.Runtime; using Amazon.S3; using Microsoft.AspNetCore.Http.Features; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.Options; using MudBlazor.Services; using Pldpro.Web.Components; using Pldpro.Web.Components.Pages; using Pldpro.Web.Models; using Pldpro.Web.Services; using System.IO; using System.Net.Http; using System.Net.Mime; using System.Net.NetworkInformation; using System.Runtime.Intrinsics.Arm; using static MudBlazor.CategoryTypes; using static MudBlazor.Colors; var builder = WebApplication.CreateBuilder(args); // Add MudBlazor services builder.Services.AddMudServices(); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); builder.Services.AddServerSideBlazor() .AddCircuitOptions(options => options.DetailedErrors = true); // HttpClient-Fabrik für serverseitige Komponenten builder.Services.AddHttpClient(); // MySQL Repository builder.Services.AddSingleton(); // --- S3 / RustFS Settings binding --- builder.Services.Configure(builder.Configuration.GetSection("S3")); // Optional: größere Uploads erlauben (z. B. 512MB) builder.Services.Configure(o => { o.MultipartBodyLengthLimit = 512L * 1024 * 1024; }); // IAmazonS3 via DI (lokaler S3-kompatibler Endpoint) builder.Services.AddSingleton(sp => { var s = sp.GetRequiredService>().Value; var cfg = new Amazon.S3.AmazonS3Config { ServiceURL = s.ServiceURL, ForcePathStyle = s.ForcePathStyle, UseHttp = s.UseHttp // AuthenticationRegion ist bei Custom-S3 i. d. R. egal } ; var creds = new BasicAWSCredentials(s.AccessKey, s.SecretKey); return new AmazonS3Client(creds, cfg); }); // Domain-Service builder.Services.AddScoped(); var app = builder.Build(); // Schema sicherstellen (einmalig beim Start) using (var scope = app.Services.CreateScope()) { var repo = scope.ServiceProvider.GetRequiredService(); await repo.EnsureSchemaAsync(); } // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseAntiforgery(); app.MapStaticAssets(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); // --- Minimal APIs für Storage --- var storage = app.MapGroup("/api/storage"); // Buckets auflisten storage.MapGet("/buckets", async (IStorageService svc) => { var result = await svc.ListBucketsAsync(); return Results.Ok(result); }); // Bucket erstellen storage.MapPost("/buckets", async (IStorageService svc, S3CreateBucketDto dto) => { if (string.IsNullOrWhiteSpace(dto.BucketName)) return Results.BadRequest("BucketName required"); await svc.CreateBucketAsync(dto.BucketName); return Results.Ok(); }).DisableAntiforgery(); // für XHR-POST ohne Token // Objekte eines Buckets auflisten storage.MapGet("/buckets/{bucket}/objects", async (IStorageService svc, string bucket) => { var objects = await svc.ListObjectsAsync(bucket); return Results.Ok(objects); }); // Datei in Bucket hochladen (Form-Data: file) storage.MapPost("/buckets/{bucket}/upload", async (HttpRequest req, IStorageService svc, IStorageMetadataRepository meta, string bucket, CancellationToken ct) => { if (!req.HasFormContentType) return Results.BadRequest("Multipart/form-data expected"); var form = await req.ReadFormAsync(); var file = form.Files["file"]; if (file is null) return Results.BadRequest("'file' missing"); var path = form["path"].ToString(); // optional, z.B. "docs/2026" path = string.IsNullOrWhiteSpace(path) ? null : path!.Trim().Trim('/'); //await using var stream = file.OpenReadStream(); // Streamlimit über FormOptions konfiguriert // await svc.UploadObjectAsync(bucket, file.FileName, stream, file.ContentType ?? "application/octet-stream"); // Key bauen: optionaler Pfad + Dateiname var key = string.IsNullOrWhiteSpace(path) ? file.FileName : $"{path}/{file.FileName}"; await using var stream = file.OpenReadStream(); var contentType = file.ContentType ?? "application/octet-stream"; await svc.UploadObjectAsync(bucket, key, stream, contentType, ct); // Metadaten persistieren await meta.UpsertAsync(bucket, file.FileName, path, key, file.Length, contentType, ct); return Results.Ok(new { bucket, file = file.FileName, path, key }); }).DisableAntiforgery(); // für Blazor XHR Upload // Objekt herunterladen storage.MapGet("/buckets/{bucket}/download/{*key}", async (IStorageService svc,string bucket,string key,CancellationToken ct) => { // key ist als Catch-all {*key} definiert, damit auch Keys mit "/" (Prefix/Ordner) funktionieren. var(stream, contentType, length) = await svc.GetObjectAsync(bucket, key, ct); // Dateiname aus Key ableiten: var fileName = key.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? key; return Results.File( fileStream: stream, contentType: contentType, fileDownloadName: fileName, enableRangeProcessing: true, // erlaubt Resume/Teildownloads lastModified: null, // optional: Last-Modified selbst setzen entityTag: null); // optional: ETag setzen }); storage.MapGet("/buckets/{bucket}/files/{fileName}/download", async ( IStorageService svc, IStorageMetadataRepository meta, string bucket, string fileName, CancellationToken ct) => { var entry = await meta.TryGetAsync(bucket, fileName, ct); if (entry is null) return Results.NotFound($"No metadata for {bucket}/{fileName}"); var (stream, contentType, _) = await svc.GetObjectAsync(bucket, entry.Key, ct); return Results.File(stream, contentType, fileName, enableRangeProcessing: true); }); storage.MapDelete("/buckets/{bucket}/objects/{*key}", async( IStorageService svc, IStorageMetadataRepository meta, string bucket, string key, CancellationToken ct) => { await svc.DeleteObjectAsync(bucket, key); await meta.DeleteByKeyAsync(bucket, key, ct); return Results.NoContent(); }).DisableAntiforgery(); app.Run();