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 }