All checks were successful
Build & Deploy PLDpro.Web Test to 192.168.1.100 / build-and-deploy (push) Successful in 1m15s
197 lines
6.5 KiB
C#
197 lines
6.5 KiB
C#
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<IStorageMetadataRepository, StorageMetadataRepository>();
|
|
|
|
|
|
|
|
// --- 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>();
|
|
builder.Services.AddScoped<Pldpro.Web.UI.Services.IDocumentClient, Pldpro.Web.UI.Services.StorageDocumentClient>();
|
|
var app = builder.Build();
|
|
|
|
|
|
// Schema sicherstellen (einmalig beim Start)
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var repo = scope.ServiceProvider.GetRequiredService<IStorageMetadataRepository>();
|
|
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<App>()
|
|
.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();
|