Update
All checks were successful
Build & Deploy PLDpro.Web Test to 192.168.1.100 / build-and-deploy (push) Successful in 1m13s

This commit is contained in:
2026-02-09 18:38:12 +01:00
parent 6008c43fec
commit d333409c19
9 changed files with 242 additions and 10 deletions

View File

@@ -9,4 +9,5 @@ public interface IStorageService
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);
Task<(Stream Stream, string ContentType, long? ContentLength)> GetObjectAsync(string bucket, string key, CancellationToken ct = default);
}

View File

@@ -33,9 +33,11 @@ public sealed class S3StorageService(IAmazonS3 s3) : IStorageService
BucketName = bucket,
ContinuationToken = token
}, ct);
items.AddRange(resp.S3Objects.Select(o => new ObjectItem(o.Key, o.Size, o.LastModified)));
token = (bool)resp.IsTruncated ? resp.NextContinuationToken : null;
if (resp.S3Objects != null)
{
items.AddRange(resp.S3Objects.Select(o => new ObjectItem(o.Key, o.Size, o.LastModified)));
token = (bool)resp.IsTruncated ? resp.NextContinuationToken : null;
}
} while (token is not null);
return items;
@@ -52,4 +54,24 @@ public sealed class S3StorageService(IAmazonS3 s3) : IStorageService
};
await _s3.PutObjectAsync(req, ct);
}
public async Task<(Stream Stream, string ContentType, long? ContentLength)> GetObjectAsync(
string bucket, string key, CancellationToken ct = default)
{
var resp = await _s3.GetObjectAsync(new GetObjectRequest
{
BucketName = bucket,
Key = key
}, ct);
// ResponseStream NICHT kopieren, sondern direkt zurückgeben (Server streamt es weiter)
var contentType = string.IsNullOrWhiteSpace(resp.Headers.ContentType)
? "application/octet-stream"
: resp.Headers.ContentType;
long? len = resp.Headers.ContentLength >= 0 ? resp.Headers.ContentLength : null;
return (resp.ResponseStream, contentType, len);
}
}

View File

@@ -0,0 +1,20 @@
namespace Pldpro.Web.Services;
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<StorageObject?> TryGetAsync(string bucket, string fileName, CancellationToken ct = default);
}
public sealed record StorageObject(
long Id,
string Bucket,
string FileName,
string? Path,
string Key,
long? Size,
string? ContentType,
DateTime CreatedUtc
);

View File

@@ -0,0 +1,93 @@
using MySqlConnector;
using System.Data;
namespace Pldpro.Web.Services;
public sealed class StorageMetadataRepository : IStorageMetadataRepository
{
private readonly string _connStr;
public StorageMetadataRepository(IConfiguration cfg) =>
_connStr = cfg.GetConnectionString("StorageDb") ?? throw new InvalidOperationException("ConnectionStrings:StorageDb missing");
public async Task EnsureSchemaAsync(CancellationToken ct = default)
{
const string sql = """
CREATE TABLE IF NOT EXISTS storage_objects (
id BIGINT NOT NULL AUTO_INCREMENT,
bucket VARCHAR(63) NOT NULL,
file_name VARCHAR(255) NOT NULL,
path VARCHAR(768) NULL,
s3_key VARCHAR(1024) NOT NULL,
size BIGINT NULL,
content_type VARCHAR(255) NULL,
created_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uq_bucket_file (bucket, file_name),
INDEX ix_bucket_path (bucket, path(255))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""";
await using var conn = new MySqlConnection(_connStr);
await conn.OpenAsync(ct);
await using var cmd = new MySqlCommand(sql, conn);
await cmd.ExecuteNonQueryAsync(ct);
}
public async Task UpsertAsync(string bucket, string fileName, string? path, string key, long? size, string? contentType, CancellationToken ct = default)
{
const string sql = """
INSERT INTO storage_objects (bucket, file_name, path, s3_key, size, content_type)
VALUES (@bucket, @name, @path, @key, @size, @ct)
ON DUPLICATE KEY UPDATE
path = VALUES(path),
s3_key = VALUES(s3_key),
size = VALUES(size),
content_type = VALUES(content_type);
""";
await using var conn = new MySqlConnection(_connStr);
await conn.OpenAsync(ct);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@bucket", bucket);
cmd.Parameters.AddWithValue("@name", fileName);
cmd.Parameters.AddWithValue("@path", (object?)path ?? DBNull.Value);
cmd.Parameters.AddWithValue("@key", key);
cmd.Parameters.AddWithValue("@size", (object?)size ?? DBNull.Value);
cmd.Parameters.AddWithValue("@ct", (object?)contentType ?? DBNull.Value);
await cmd.ExecuteNonQueryAsync(ct);
}
public async Task<StorageObject?> TryGetAsync(string bucket, string fileName, CancellationToken ct = default)
{
const string sql = """
SELECT id, bucket, file_name, path, s3_key, size, content_type, created_utc
FROM storage_objects
WHERE bucket = @bucket AND file_name = @name
LIMIT 1;
""";
await using var conn = new MySqlConnection(_connStr);
await conn.OpenAsync(ct);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@bucket", bucket);
cmd.Parameters.AddWithValue("@name", fileName);
await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, ct);
if (await reader.ReadAsync(ct))
{
return new StorageObject(
reader.GetInt64(0),
reader.GetString(1),
reader.GetString(2),
reader.IsDBNull(3) ? null : reader.GetString(3),
reader.GetString(4),
reader.IsDBNull(5) ? null : reader.GetInt64(5),
reader.IsDBNull(6) ? null : reader.GetString(6),
reader.GetDateTime(7)
);
}
return null;
}
}