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 0000000..4175989
Binary files /dev/null and b/Program.txt differ
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
+ }
+
}