From 46d7b7e6d8f5d45598faa306057978f2986eb824 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 24 Jan 2026 20:46:25 +0100 Subject: [PATCH] S3 Test --- Components/Layout/NavMenu.razor | 2 +- Components/Pages/Storage.razor | 116 +++++++++++++++++++++++++++ Components/_Imports.razor | 1 + Models/S3Settings.cs | 12 +++ Pldpro.Web.csproj | 4 +- Program.cs | 79 ++++++++++++++++++ Program.txt | Bin 0 -> 14108 bytes Services/IStorageService.cs | 12 +++ Services/Models/BucketItem.cs | 4 + Services/Models/ObjectItem.cs | 4 + Services/Models/S3CreateBucketDto.cs | 7 ++ Services/S3StorageService.cs | 55 +++++++++++++ appsettings.json | 11 ++- 13 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 Components/Pages/Storage.razor create mode 100644 Models/S3Settings.cs create mode 100644 Program.txt create mode 100644 Services/IStorageService.cs create mode 100644 Services/Models/BucketItem.cs create mode 100644 Services/Models/ObjectItem.cs create mode 100644 Services/Models/S3CreateBucketDto.cs create mode 100644 Services/S3StorageService.cs diff --git a/Components/Layout/NavMenu.razor b/Components/Layout/NavMenu.razor index c37a619..33d63a2 100644 --- a/Components/Layout/NavMenu.razor +++ b/Components/Layout/NavMenu.razor @@ -6,7 +6,7 @@ Weather Test - + Storage diff --git a/Components/Pages/Storage.razor b/Components/Pages/Storage.razor new file mode 100644 index 0000000..e2e64a5 --- /dev/null +++ b/Components/Pages/Storage.razor @@ -0,0 +1,116 @@ + +@page "/storage" +@inject HttpClient Http +@using System.Net.Http.Json +@using Pldpro.Web.Models + +Storage + +S3 Storage + + + + Buckets + + + Erstellen + + + + @if (buckets is null) + { + (lädt...) + } + else if (!buckets.Any()) + { + (keine Buckets) + } + else + { + @foreach (var b in buckets) + { + + + @b.Name + + } + } + + + +@if (!string.IsNullOrEmpty(selectedBucket)) +{ + + + Objekte in '@selectedBucket' + + + + + + + Key + Größe + Geändert + + + @context.Key + @context.Size + @context.LastModified + + + +} + +@code { + private record BucketVm(string Name, DateTime? CreationDate); + private record ObjectVm(string Key, long? Size, DateTime? LastModified); + + private List? buckets; + private List? objects; + private string? selectedBucket; + private string newBucketName = ""; + private const long StreamLimit = 512L * 1024 * 1024; // 512 MB (Program.cs erhöht Multipart-Limit) + + protected override async Task OnInitializedAsync() + => await LoadBuckets(); + + private async Task LoadBuckets() + { + var data = await Http.GetFromJsonAsync>("/api/storage/buckets"); + buckets = data ?? new(); + StateHasChanged(); + } + + private async Task SelectBucket(string name) + { + selectedBucket = name; + objects = await Http.GetFromJsonAsync>($"/api/storage/buckets/{name}/objects") ?? new(); + } + + private async Task CreateBucket() + { + if (string.IsNullOrWhiteSpace(newBucketName)) return; + await Http.PostAsJsonAsync("/api/storage/buckets", new S3CreateBucketDto { BucketName = newBucketName! }); + newBucketName = ""; + await LoadBuckets(); + } + + private async Task OnFilesSelected(InputFileChangeEventArgs e) + { + if (string.IsNullOrEmpty(selectedBucket)) return; + + foreach (var file in e.GetMultipleFiles()) + { + using var stream = file.OpenReadStream(StreamLimit); + using var content = new MultipartFormDataContent(); + content.Add(new StreamContent(stream), "file", file.Name); + + var resp = await Http.PostAsync($"/api/storage/buckets/{selectedBucket}/upload", content); + resp.EnsureSuccessStatusCode(); + } + // Refresh list + objects = await Http.GetFromJsonAsync>($"/api/storage/buckets/{selectedBucket}/objects") ?? new(); + } +} diff --git a/Components/_Imports.razor b/Components/_Imports.razor index 8fdb8b6..38ad5b7 100644 --- a/Components/_Imports.razor +++ b/Components/_Imports.razor @@ -10,3 +10,4 @@ @using MudBlazor.Services @using Pldpro.Web @using Pldpro.Web.Components +@using Amazon.S3 \ No newline at end of file diff --git a/Models/S3Settings.cs b/Models/S3Settings.cs new file mode 100644 index 0000000..0362d49 --- /dev/null +++ b/Models/S3Settings.cs @@ -0,0 +1,12 @@ + +namespace Pldpro.Web.Models; + +public sealed class S3Settings +{ + public string ServiceURL { get; set; } = string.Empty; + public string AccessKey { get; set; } = string.Empty; + public string SecretKey { get; set; } = string.Empty; + public bool UseHttp { get; set; } = true; + public bool ForcePathStyle { get; set; } = true; + public string? DefaultBucketPrefix { get; set; } +} diff --git a/Pldpro.Web.csproj b/Pldpro.Web.csproj index 20b9f68..039cf14 100644 --- a/Pldpro.Web.csproj +++ b/Pldpro.Web.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -8,6 +8,8 @@ + + diff --git a/Program.cs b/Program.cs index 092c59c..963b664 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,17 @@ +using Amazon.Runtime; +using Amazon.S3; +using Microsoft.AspNetCore.Http.Features; +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.Net.NetworkInformation; +using System.Runtime.Intrinsics.Arm; +using static MudBlazor.CategoryTypes; +using static MudBlazor.Colors; + var builder = WebApplication.CreateBuilder(args); @@ -10,6 +22,32 @@ builder.Services.AddMudServices(); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); + +// --- 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(); // Configure the HTTP request pipeline. @@ -29,4 +67,45 @@ 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, string bucket) => +{ + 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"); + + await using var stream = file.OpenReadStream(); // Streamlimit über FormOptions konfiguriert + await svc.UploadObjectAsync(bucket, file.FileName, stream, file.ContentType ?? "application/octet-stream"); + return Results.Ok(); + }).DisableAntiforgery(); // für Blazor XHR Upload + + + app.Run(); diff --git a/Program.txt b/Program.txt new file mode 100644 index 0000000000000000000000000000000000000000..4175989a5bbb96718e5e131ce1150aac29dcb457 GIT binary patch literal 14108 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 literal 0 HcmV?d00001 diff --git a/Services/IStorageService.cs b/Services/IStorageService.cs new file mode 100644 index 0000000..8640212 --- /dev/null +++ b/Services/IStorageService.cs @@ -0,0 +1,12 @@ + +using Pldpro.Web.Services.Models; + +namespace Pldpro.Web.Services; + +public interface IStorageService +{ + Task> ListBucketsAsync(CancellationToken ct = default); + Task CreateBucketAsync(string bucketName, CancellationToken ct = default); + Task> ListObjectsAsync(string bucket, CancellationToken ct = default); + Task UploadObjectAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default); +} diff --git a/Services/Models/BucketItem.cs b/Services/Models/BucketItem.cs new file mode 100644 index 0000000..3521aaf --- /dev/null +++ b/Services/Models/BucketItem.cs @@ -0,0 +1,4 @@ + +namespace Pldpro.Web.Services.Models; + +public sealed record BucketItem(string Name, DateTime? CreationDate); diff --git a/Services/Models/ObjectItem.cs b/Services/Models/ObjectItem.cs new file mode 100644 index 0000000..50b3981 --- /dev/null +++ b/Services/Models/ObjectItem.cs @@ -0,0 +1,4 @@ + +namespace Pldpro.Web.Services.Models; + +public sealed record ObjectItem(string Key, long? Size, DateTime? LastModified); diff --git a/Services/Models/S3CreateBucketDto.cs b/Services/Models/S3CreateBucketDto.cs new file mode 100644 index 0000000..d83e30c --- /dev/null +++ b/Services/Models/S3CreateBucketDto.cs @@ -0,0 +1,7 @@ + +namespace Pldpro.Web.Models; + +public sealed class S3CreateBucketDto +{ + public string BucketName { get; set; } = string.Empty; +} diff --git a/Services/S3StorageService.cs b/Services/S3StorageService.cs new file mode 100644 index 0000000..dc27365 --- /dev/null +++ b/Services/S3StorageService.cs @@ -0,0 +1,55 @@ + +using Amazon.S3; +using Amazon.S3.Model; +using Pldpro.Web.Services.Models; + +namespace Pldpro.Web.Services; + +public sealed class S3StorageService(IAmazonS3 s3) : IStorageService +{ + private readonly IAmazonS3 _s3 = s3; + + public async Task> ListBucketsAsync(CancellationToken ct = default) + { + var resp = await _s3.ListBucketsAsync(ct); + return resp.Buckets.Select(b => new BucketItem(b.BucketName, b.CreationDate)); + } + + public async Task CreateBucketAsync(string bucketName, CancellationToken ct = default) + { + // Für S3-kompatible Endpoints reicht häufig nur der Name + var req = new PutBucketRequest { BucketName = bucketName }; + await _s3.PutBucketAsync(req, ct); + } + + public async Task> ListObjectsAsync(string bucket, CancellationToken ct = default) + { + var items = new List(); + string? token = null; + do + { + var resp = await _s3.ListObjectsV2Async(new ListObjectsV2Request + { + BucketName = bucket, + ContinuationToken = token + }, ct); + + items.AddRange(resp.S3Objects.Select(o => new ObjectItem(o.Key, o.Size, o.LastModified))); + token = resp.IsTruncated ? resp.NextContinuationToken : null; + } while (token is not null); + + return items; + } + + public async Task UploadObjectAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default) + { + var req = new PutObjectRequest + { + BucketName = bucket, + Key = key, + InputStream = content, + ContentType = contentType + }; + await _s3.PutObjectAsync(req, ct); + } +} diff --git a/appsettings.json b/appsettings.json index 10f68b8..9c1866e 100644 --- a/appsettings.json +++ b/appsettings.json @@ -5,5 +5,14 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "S3": { + "ServiceURL": "http://192.168.1.102:9000", + "AccessKey": "your-access-key", + "SecretKey": "your-secret-key", + "UseHttp": true, + "ForcePathStyle": true, + "DefaultBucketPrefix": "pld-" // optional + } + }