diff --git a/Components/Pages/Storage.razor b/Components/Pages/Storage.razor index 2858694..13f1728 100644 --- a/Components/Pages/Storage.razor +++ b/Components/Pages/Storage.razor @@ -57,6 +57,7 @@ Geändert + @context.Key @@ -73,10 +74,17 @@ } Download by Name + + + + Löschen + + -} + } @code { private record BucketVm(string Name, DateTime? CreationDate); @@ -148,5 +156,42 @@ return $"/api/storage/buckets/{selectedBucket}/download/{encodedKey}"; } - + + private async Task ConfirmAndDelete(string key) + { + // Simple Bestätigung; alternativ MudDialog verwenden + var really = await JSConfirm($"Objekt löschen?\n\nBucket: {selectedBucket}\nKey: {key}"); + if (really) + { + await DeleteObject(key); + } + } + + + private async Task DeleteObject(string key) + { + if (string.IsNullOrEmpty(selectedBucket)) return; + + var encodedKey = Uri.EscapeDataString(key); + var url = $"/api/storage/buckets/{selectedBucket}/objects/{encodedKey}"; + + var resp = await Http!.DeleteAsync(url); + if (resp.IsSuccessStatusCode) + { + // Liste aktualisieren + objects = await Http!.GetFromJsonAsync>($"/api/storage/buckets/{selectedBucket}/objects") ?? new(); + StateHasChanged(); + } + else + { + var msg = await resp.Content.ReadAsStringAsync(); + throw new InvalidOperationException($"Delete fehlgeschlagen: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{msg}"); + } + } + + // Sehr einfache JS-Confirm-Hilfe (füge IJSRuntime-Injection hinzu) + [Inject] private IJSRuntime JS { get; set; } = default!; + private async Task JSConfirm(string message) + => await JS.InvokeAsync("confirm", message); } + diff --git a/Program.cs b/Program.cs index 819fa0b..bf86c26 100644 --- a/Program.cs +++ b/Program.cs @@ -178,5 +178,19 @@ storage.MapGet("/buckets/{bucket}/files/{fileName}/download", async ( }); +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(); + }); + + + app.Run(); diff --git a/Services.txt b/Services.txt new file mode 100644 index 0000000..deeedd2 Binary files /dev/null and b/Services.txt differ diff --git a/Services/IStorageService.cs b/Services/IStorageService.cs index 5655bbb..40f9108 100644 --- a/Services/IStorageService.cs +++ b/Services/IStorageService.cs @@ -9,5 +9,6 @@ public interface IStorageService 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); + Task DeleteObjectAsync(string bucket, string key); Task<(Stream Stream, string ContentType, long? ContentLength)> GetObjectAsync(string bucket, string key, CancellationToken ct = default); } diff --git a/Services/S3StorageService.cs b/Services/S3StorageService.cs index 7a58261..055f10b 100644 --- a/Services/S3StorageService.cs +++ b/Services/S3StorageService.cs @@ -74,4 +74,15 @@ public sealed class S3StorageService(IAmazonS3 s3) : IStorageService return (resp.ResponseStream, contentType, len); } + + public async Task DeleteObjectAsync(string bucket, string key) + { + // S3-Delete ist idempotent: 204 auch wenn das Objekt nicht existiert. + await _s3.DeleteObjectAsync(new Amazon.S3.Model.DeleteObjectRequest + { + BucketName = bucket, + Key = key + }); + } + } diff --git a/Services/Storage/IStorageMetadataRepository.cs b/Services/Storage/IStorageMetadataRepository.cs index 7d590b5..065ad92 100644 --- a/Services/Storage/IStorageMetadataRepository.cs +++ b/Services/Storage/IStorageMetadataRepository.cs @@ -6,6 +6,7 @@ public interface IStorageMetadataRepository Task EnsureSchemaAsync(CancellationToken ct = default); Task UpsertAsync(string bucket, string fileName, string? path, string key, long? size, string? contentType, CancellationToken ct = default); Task TryGetAsync(string bucket, string fileName, CancellationToken ct = default); + Task DeleteByKeyAsync(string bucket, string key, CancellationToken ct = default); } public sealed record StorageObject( diff --git a/Services/Storage/StorageMetaDataRepository.cs b/Services/Storage/StorageMetaDataRepository.cs index 3027396..5f3ddb2 100644 --- a/Services/Storage/StorageMetaDataRepository.cs +++ b/Services/Storage/StorageMetaDataRepository.cs @@ -90,4 +90,19 @@ public sealed class StorageMetadataRepository : IStorageMetadataRepository } return null; } + + public async Task DeleteByKeyAsync(string bucket, string key, CancellationToken ct = default) + { + const string sql = """ + DELETE FROM storage_objects + WHERE bucket = @bucket AND s3_key = @key; + """; + await using var conn = new MySqlConnector.MySqlConnection(_connStr); + await conn.OpenAsync(ct); + await using var cmd = new MySqlConnector.MySqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@bucket", bucket); + cmd.Parameters.AddWithValue("@key", key); + await cmd.ExecuteNonQueryAsync(ct); + } + }