- mail/internal/storage: LocalDriver/S3Driver (bewaehrtes Muster aus DMS FDN-03, bewusste Neuimplementierung - Mail kann DMS nicht importieren), ObjectKey mit festem Pfadschema - Service.Put/GetVerified: Pruefsummenverifikation AN DIESER SCHICHT (Erweiterung gegenueber FDN-03) - SHA-256-Sidecar, sofortige Ruecklese-Verifikation beim Schreiben, Erkennung manipulierter Objekte beim Lesen - HTTPUsageReporter: meldet an Core API-11 (resync-api/LIC-05), identisches Muster wie DMS FDN-03 - 4 Tests real bestanden: byteidentischer Read-back, manipuliertes Objekt erkannt, Lasttest (500 Objekte, 105.8us/Objekt), Nutzungsmeldung bei Schreiben+Loeschen - zusaetzlich echter End-zu-Ende-Beweis gegen den laufenden nexarch-resync-api.service: reales Service-Credential provisioniert, Put->GetVerified->Delete komplett durchlaufen, usage_counters zeigt reales +29/-29-Delta (beide Meldungen real angewendet) Pruefungen siehe mail/docs/ARC-01-PRUEFPROTOKOLL.md
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/aws/smithy-go"
|
|
)
|
|
|
|
// S3Driver legt Objekte in einem S3-kompatiblen Objektspeicher ab — der
|
|
// Produktions-Treiber (Akzeptanzkriterium 1). Funktioniert gegen echtes
|
|
// AWS S3 UND gegen jeden S3-kompatiblen Anbieter (MinIO etc.) über
|
|
// endpointURL. Gleiches, bewährtes Muster wie DMS FDN-03s S3Driver
|
|
// (bewusste Kopie, Mail kann DMS nicht importieren).
|
|
type S3Driver struct {
|
|
client *s3.Client
|
|
bucket string
|
|
}
|
|
|
|
func NewS3Driver(ctx context.Context, bucket, region, endpointURL, accessKeyID, secretAccessKey string, usePathStyle bool) (*S3Driver, error) {
|
|
cfg, err := config.LoadDefaultConfig(ctx,
|
|
config.WithRegion(region),
|
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, "")),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: s3-konfiguration laden: %w", err)
|
|
}
|
|
|
|
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
|
if endpointURL != "" {
|
|
o.BaseEndpoint = aws.String(endpointURL)
|
|
}
|
|
o.UsePathStyle = usePathStyle
|
|
})
|
|
return &S3Driver{client: client, bucket: bucket}, nil
|
|
}
|
|
|
|
func (d *S3Driver) Put(ctx context.Context, key string, r io.Reader, _ int64, contentType string) (int64, error) {
|
|
buf, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("storage: objekt vor upload lesen: %w", err)
|
|
}
|
|
_, err = d.client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(d.bucket),
|
|
Key: aws.String(key),
|
|
Body: bytes.NewReader(buf),
|
|
ContentLength: aws.Int64(int64(len(buf))),
|
|
ContentType: aws.String(contentType),
|
|
})
|
|
if err != nil {
|
|
return 0, fmt.Errorf("storage: s3-upload: %w", err)
|
|
}
|
|
return int64(len(buf)), nil
|
|
}
|
|
|
|
func (d *S3Driver) Get(ctx context.Context, key string) (io.ReadCloser, error) {
|
|
out, err := d.client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(d.bucket),
|
|
Key: aws.String(key),
|
|
})
|
|
if err != nil {
|
|
if isS3NotFound(err) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("storage: s3-download: %w", err)
|
|
}
|
|
return out.Body, nil
|
|
}
|
|
|
|
func (d *S3Driver) Delete(ctx context.Context, key string) error {
|
|
// S3 liefert bei DeleteObject fuer ein nicht existierendes Objekt
|
|
// KEINEN Fehler (idempotente S3-API-Semantik) — um denselben
|
|
// Vertrag wie LocalDriver (ErrNotFound bei fehlendem Objekt) zu
|
|
// erfüllen, wird die Existenz vorher explizit geprüft.
|
|
_, err := d.client.HeadObject(ctx, &s3.HeadObjectInput{Bucket: aws.String(d.bucket), Key: aws.String(key)})
|
|
if err != nil {
|
|
if isS3NotFound(err) {
|
|
return ErrNotFound
|
|
}
|
|
return fmt.Errorf("storage: s3-existenzprüfung vor löschen: %w", err)
|
|
}
|
|
|
|
if _, err := d.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: aws.String(d.bucket),
|
|
Key: aws.String(key),
|
|
}); err != nil {
|
|
return fmt.Errorf("storage: s3-löschen: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isS3NotFound(err error) bool {
|
|
var nsk *types.NoSuchKey
|
|
if errors.As(err, &nsk) {
|
|
return true
|
|
}
|
|
var apiErr smithy.APIError
|
|
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "NotFound" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|