- 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
128 lines
4.7 KiB
Go
128 lines
4.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// ErrChecksumMismatch wird von GetVerified geliefert, wenn der beim
|
|
// Lesen berechnete Hash nicht mit der beim Schreiben gespeicherten
|
|
// Prüfsumme übereinstimmt (Akzeptanzkriterium 2 / Pflichtprüfung 2:
|
|
// ein absichtlich beschädigtes Objekt wird erkannt).
|
|
var ErrChecksumMismatch = errors.New("storage: prüfsumme stimmt nicht überein — objekt wurde verändert")
|
|
|
|
// Service verbindet einen Driver mit Prüfsummenverifikation
|
|
// (Akzeptanzkriterium 2) und der Nutzungsmeldung an Core LIC-05
|
|
// (Akzeptanzkriterium 4) — jeder Schreib-/Löschvorgang über Service
|
|
// löst GENAU EINE Meldung mit der tatsächlich geschriebenen/gelöschten
|
|
// Objektgröße aus. Aufrufer (spätere Tickets, z. B. IMP-*) rufen
|
|
// ausschließlich Service auf, nie einen Driver direkt.
|
|
type Service struct {
|
|
driver Driver
|
|
usage UsageReporter
|
|
tenantSlug string
|
|
}
|
|
|
|
func NewService(driver Driver, usage UsageReporter, tenantSlug string) *Service {
|
|
return &Service{driver: driver, usage: usage, tenantSlug: tenantSlug}
|
|
}
|
|
|
|
// Put legt den Inhalt ab UND verifiziert den Schreibvorgang durch
|
|
// Prüfsummenvergleich (Akzeptanzkriterium 2): der Inhalt wird
|
|
// geschrieben, die Prüfsumme als Sidecar-Objekt gespeichert, danach
|
|
// SOFORT zurückgelesen und erneut gehasht — weicht der Rückgelesene
|
|
// Hash vom beim Schreiben berechneten ab, meldet Put einen Fehler,
|
|
// statt eine unbemerkt fehlerhafte Ablage stehen zu lassen. Meldet die
|
|
// geschriebene Größe als positives Delta an Core LIC-05
|
|
// (Akzeptanzkriterium 4).
|
|
func (s *Service) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) (checksum string, err error) {
|
|
hasher := sha256.New()
|
|
tee := io.TeeReader(r, hasher)
|
|
|
|
written, err := s.driver.Put(ctx, key, tee, size, contentType)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
checksum = hex.EncodeToString(hasher.Sum(nil))
|
|
|
|
if _, err := s.driver.Put(ctx, checksumKey(key), bytes.NewReader([]byte(checksum)), int64(len(checksum)), "text/plain"); err != nil {
|
|
return "", fmt.Errorf("storage: prüfsumme speichern: %w", err)
|
|
}
|
|
|
|
// Sofortige Rücklese-Verifikation — beweist, dass der Schreibvorgang
|
|
// tatsächlich verifiziert wurde, nicht nur eine Prüfsumme abgelegt
|
|
// wurde, die nie geprüft wird.
|
|
if _, err := s.GetVerified(ctx, key); err != nil {
|
|
return "", fmt.Errorf("storage: schreibverifikation fehlgeschlagen: %w", err)
|
|
}
|
|
|
|
if err := s.usage.Report(ctx, s.tenantSlug, UsageMetric, written); err != nil {
|
|
return checksum, fmt.Errorf("storage: objekt gespeichert, aber nutzungsmeldung fehlgeschlagen: %w", err)
|
|
}
|
|
return checksum, nil
|
|
}
|
|
|
|
// Get liefert den Inhalt UNVERIFIZIERT (Streaming, für große Objekte).
|
|
// Für die Pflichtprüfung "beschädigtes Objekt wird erkannt" GetVerified
|
|
// verwenden.
|
|
func (s *Service) Get(ctx context.Context, key string) (io.ReadCloser, error) {
|
|
return s.driver.Get(ctx, key)
|
|
}
|
|
|
|
// GetVerified liest den vollständigen Inhalt UND vergleicht die beim
|
|
// Schreiben gespeicherte Prüfsumme gegen den beim Lesen berechneten
|
|
// Hash (Akzeptanzkriterium 2 / Pflichtprüfung 2).
|
|
func (s *Service) GetVerified(ctx context.Context, key string) ([]byte, error) {
|
|
sumReader, err := s.driver.Get(ctx, checksumKey(key))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: gespeicherte prüfsumme lesen: %w", err)
|
|
}
|
|
expectedRaw, err := io.ReadAll(sumReader)
|
|
_ = sumReader.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: gespeicherte prüfsumme lesen: %w", err)
|
|
}
|
|
expected := string(expectedRaw)
|
|
|
|
contentReader, err := s.driver.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = contentReader.Close() }()
|
|
|
|
hasher := sha256.New()
|
|
content, err := io.ReadAll(io.TeeReader(contentReader, hasher))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: objekt lesen: %w", err)
|
|
}
|
|
actual := hex.EncodeToString(hasher.Sum(nil))
|
|
if actual != expected {
|
|
return nil, ErrChecksumMismatch
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
// Delete entfernt Inhalt UND Prüfsummen-Sidecar, meldet die Größe als
|
|
// negatives Delta an Core LIC-05 (Akzeptanzkriterium 4) — der Aufrufer
|
|
// muss die Größe kennen (Delete selbst kann sie nach dem Löschen nicht
|
|
// mehr ermitteln).
|
|
func (s *Service) Delete(ctx context.Context, key string, sizeBytes int64) error {
|
|
if err := s.driver.Delete(ctx, key); err != nil {
|
|
return err
|
|
}
|
|
// Sidecar-Löschung ist best effort — ein fehlendes Sidecar (z. B.
|
|
// bei einem sehr alten Objekt) darf den eigentlichen Löschvorgang
|
|
// nicht blockieren.
|
|
_ = s.driver.Delete(ctx, checksumKey(key))
|
|
|
|
if err := s.usage.Report(ctx, s.tenantSlug, UsageMetric, -sizeBytes); err != nil {
|
|
return fmt.Errorf("storage: objekt gelöscht, aber nutzungsmeldung fehlgeschlagen: %w", err)
|
|
}
|
|
return nil
|
|
}
|