@@ -6,7 +6,7 @@
|
|||||||
<MudNavLink Href="weather" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Weather</MudNavLink>
|
<MudNavLink Href="weather" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Weather</MudNavLink>
|
||||||
|
|
||||||
<MudNavLink Href="test" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Test</MudNavLink>
|
<MudNavLink Href="test" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Test</MudNavLink>
|
||||||
<MudNavLink Href="storage" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Cloud">Storage</MudNavLink>
|
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
|
|
||||||
@page "/storage"
|
|
||||||
@inject HttpClient Http
|
|
||||||
@using System.Net.Http.Json
|
|
||||||
@using Pldpro.Web.Models
|
|
||||||
|
|
||||||
<PageTitle>Storage</PageTitle>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" GutterBottom="true">S3 Storage</MudText>
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="0">
|
|
||||||
<MudStack Row="true" Spacing="2" AlignItems="center">
|
|
||||||
<MudText Typo="Typo.h6">Buckets</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudTextField @bind-Value="newBucketName" Placeholder="neuer Bucketname" Variant="Variant.Outlined" />
|
|
||||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="CreateBucket">Erstellen</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<MudList Dense="true" Class="mt-2">
|
|
||||||
@if (buckets is null)
|
|
||||||
{
|
|
||||||
<MudListItem>(lädt...)</MudListItem>
|
|
||||||
}
|
|
||||||
else if (!buckets.Any())
|
|
||||||
{
|
|
||||||
<MudListItem>(keine Buckets)</MudListItem>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@foreach (var b in buckets)
|
|
||||||
{
|
|
||||||
<MudListItem OnClick="@(() => SelectBucket(b.Name))" Class="cursor-pointer"
|
|
||||||
Style="@(selectedBucket == b.Name ? "font-weight:600" : "")">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Class="me-2" />
|
|
||||||
@b.Name
|
|
||||||
</MudListItem>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</MudList>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(selectedBucket))
|
|
||||||
{
|
|
||||||
<MudPaper Class="pa-4" Elevation="0">
|
|
||||||
<MudStack Row="true" Spacing="2" AlignItems="center">
|
|
||||||
<MudText Typo="Typo.h6">Objekte in '@selectedBucket'</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<InputFile OnChange="OnFilesSelected" />
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<MudTable Items="objects" Hover="true" Dense="true" Class="mt-2">
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Key</MudTh>
|
|
||||||
<MudTh>Größe</MudTh>
|
|
||||||
<MudTh>Geändert</MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd DataLabel="Key">@context.Key</MudTd>
|
|
||||||
<MudTd DataLabel="Größe">@context.Size</MudTd>
|
|
||||||
<MudTd DataLabel="Geändert">@context.LastModified</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
</MudTable>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private record BucketVm(string Name, DateTime? CreationDate);
|
|
||||||
private record ObjectVm(string Key, long? Size, DateTime? LastModified);
|
|
||||||
|
|
||||||
private List<BucketVm>? buckets;
|
|
||||||
private List<ObjectVm>? 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<List<BucketVm>>("/api/storage/buckets");
|
|
||||||
buckets = data ?? new();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SelectBucket(string name)
|
|
||||||
{
|
|
||||||
selectedBucket = name;
|
|
||||||
objects = await Http.GetFromJsonAsync<List<ObjectVm>>($"/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<List<ObjectVm>>($"/api/storage/buckets/{selectedBucket}/objects") ?? new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,4 +10,3 @@
|
|||||||
@using MudBlazor.Services
|
@using MudBlazor.Services
|
||||||
@using Pldpro.Web
|
@using Pldpro.Web
|
||||||
@using Pldpro.Web.Components
|
@using Pldpro.Web.Components
|
||||||
@using Amazon.S3
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.3.21" />
|
|
||||||
<PackageReference Include="AWSSDK.S3" Version="4.0.17.3" />
|
|
||||||
<PackageReference Include="MudBlazor" Version="8.*" />
|
<PackageReference Include="MudBlazor" Version="8.*" />
|
||||||
<PackageReference Include="OpenIddict" Version="7.2.0" />
|
<PackageReference Include="OpenIddict" Version="7.2.0" />
|
||||||
<PackageReference Include="OpenIddict.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="OpenIddict.AspNetCore" Version="7.2.0" />
|
||||||
|
|||||||
79
Program.cs
79
Program.cs
@@ -1,17 +1,5 @@
|
|||||||
using Amazon.Runtime;
|
|
||||||
using Amazon.S3;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using Pldpro.Web.Components;
|
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);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -22,32 +10,6 @@ builder.Services.AddMudServices();
|
|||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
|
||||||
// --- S3 / RustFS Settings binding ---
|
|
||||||
builder.Services.Configure<S3Settings>(builder.Configuration.GetSection("S3"));
|
|
||||||
|
|
||||||
// Optional: größere Uploads erlauben (z. B. 512MB)
|
|
||||||
builder.Services.Configure<FormOptions>(o => { o.MultipartBodyLengthLimit = 512L * 1024 * 1024; });
|
|
||||||
|
|
||||||
// IAmazonS3 via DI (lokaler S3-kompatibler Endpoint)
|
|
||||||
builder.Services.AddSingleton<IAmazonS3>(sp =>
|
|
||||||
{
|
|
||||||
var s = sp.GetRequiredService<IOptions<S3Settings>>().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<IStorageService, S3StorageService>();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
@@ -67,45 +29,4 @@ app.MapStaticAssets();
|
|||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode();
|
.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();
|
app.Run();
|
||||||
|
|||||||
BIN
Program.txt
BIN
Program.txt
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
using Pldpro.Web.Services.Models;
|
|
||||||
|
|
||||||
namespace Pldpro.Web.Services;
|
|
||||||
|
|
||||||
public interface IStorageService
|
|
||||||
{
|
|
||||||
Task<IEnumerable<BucketItem>> ListBucketsAsync(CancellationToken ct = default);
|
|
||||||
Task CreateBucketAsync(string bucketName, CancellationToken ct = default);
|
|
||||||
Task<IEnumerable<ObjectItem>> ListObjectsAsync(string bucket, CancellationToken ct = default);
|
|
||||||
Task UploadObjectAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
namespace Pldpro.Web.Services.Models;
|
|
||||||
|
|
||||||
public sealed record BucketItem(string Name, DateTime? CreationDate);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
namespace Pldpro.Web.Services.Models;
|
|
||||||
|
|
||||||
public sealed record ObjectItem(string Key, long? Size, DateTime? LastModified);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
namespace Pldpro.Web.Models;
|
|
||||||
|
|
||||||
public sealed class S3CreateBucketDto
|
|
||||||
{
|
|
||||||
public string BucketName { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
|
|
||||||
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<IEnumerable<BucketItem>> 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<IEnumerable<ObjectItem>> ListObjectsAsync(string bucket, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var items = new List<ObjectItem>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,5 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user