FDN-03: objekt-storage-abstraktion
Ein Driver-Interface, zwei austauschbare Treiber: LocalDriver (Entwicklung, Dateisystem, HMAC-signierte URLs) und S3Driver (Produktion, S3-kompatibel via aws-sdk-go-v2, echte presigned URLs). Pfadschema documents/<id>/ revisions/<id> innerhalb des mandantenspezifischen Buckets. Service verbindet Driver mit Nutzungsmeldung an Core LIC-05 (HTTPUsageReporter, Vertrag von internal/resync.Handler nachgebildet, DMS kann Cores internal/- Pakete als eigenes Modul nicht importieren). Auf 192.168.1.131 verifiziert, S3-Treiber gegen echtes lokal installiertes MinIO (kein Mock): Round-Trip beide Treiber, abgelaufene presigned URL real mit 403 abgewiesen (manuell zusaetzlich zum Unit-Test verifiziert), klare ErrNotFound bei fehlendem Objekt beide Treiber, Nutzungsmeldung mit korrektem Tenant/Metrik/Delta bei Put/Delete. Befund dokumentiert: Cores internal/resync.Handler (Gegenstelle fuer die Nutzungsmeldung) ist noch in keinem cmd/*/main.go verdrahtet (dieselbe Fehlerklasse wie QA-05/AUD-06) - HTTPUsageReporter daher gegen den dokumentierten Vertrag getestet, nicht gegen eine laufende Core-Instanz. Siehe dms/docs/FDN-03-PRUEFPROTOKOLL.md. golangci-lint auf v2.1.6 aktualisiert (v1.63.4 konnte go1.24-Zielstand nicht linten), .golangci.yml auf v2-Konfigurationsformat migriert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9d4c2bae4a
commit
442bb674e9
@@ -0,0 +1,113 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrURLExpired wird von VerifySignedURL geliefert, wenn eine signierte URL
|
||||
// nach Ablauf ihrer Gueltigkeit verwendet wird (Pruefung 2).
|
||||
var ErrURLExpired = errors.New("storage: signierte url ist abgelaufen")
|
||||
|
||||
// ErrInvalidSignature wird geliefert, wenn die Signatur einer URL nicht zum
|
||||
// Schluessel passt (manipulierte oder falsche URL).
|
||||
var ErrInvalidSignature = errors.New("storage: signatur der url ist ungueltig")
|
||||
|
||||
// LocalDriver legt Objekte im lokalen Dateisystem ab — der Entwicklungs-
|
||||
// Treiber (Akzeptanzkriterium 1), keine externe Abhaengigkeit noetig.
|
||||
type LocalDriver struct {
|
||||
baseDir string
|
||||
signingSecret []byte
|
||||
publicBaseURL string
|
||||
}
|
||||
|
||||
// NewLocalDriver erzeugt einen LocalDriver. signingSecret authentifiziert
|
||||
// die von SignedURL ausgestellten URLs (HMAC-SHA256, konstant-zeit-
|
||||
// verglichen bei der Verifikation — timing-safe wie projektweite Konvention,
|
||||
// siehe Core IAM-15).
|
||||
func NewLocalDriver(baseDir string, signingSecret []byte, publicBaseURL string) *LocalDriver {
|
||||
return &LocalDriver{baseDir: baseDir, signingSecret: signingSecret, publicBaseURL: publicBaseURL}
|
||||
}
|
||||
|
||||
func (d *LocalDriver) path(key string) string {
|
||||
return filepath.Join(d.baseDir, filepath.FromSlash(key))
|
||||
}
|
||||
|
||||
func (d *LocalDriver) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) (int64, error) {
|
||||
full := d.path(key)
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||
return 0, fmt.Errorf("storage: verzeichnis anlegen: %w", err)
|
||||
}
|
||||
f, err := os.Create(full)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("storage: datei anlegen: %w", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
written, err := io.Copy(f, r)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("storage: schreiben: %w", err)
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (d *LocalDriver) Get(ctx context.Context, key string) (io.ReadCloser, error) {
|
||||
f, err := os.Open(d.path(key))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("storage: lesen: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *LocalDriver) Delete(ctx context.Context, key string) error {
|
||||
if err := os.Remove(d.path(key)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("storage: loeschen: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LocalDriver) SignedURL(ctx context.Context, key string, ttl time.Duration) (string, error) {
|
||||
expiry := time.Now().Add(ttl).Unix()
|
||||
sig := d.sign(key, expiry)
|
||||
return fmt.Sprintf("%s/%s?exp=%d&sig=%s", strings.TrimRight(d.publicBaseURL, "/"), key, expiry, sig), nil
|
||||
}
|
||||
|
||||
func (d *LocalDriver) sign(key string, expiry int64) string {
|
||||
mac := hmac.New(sha256.New, d.signingSecret)
|
||||
mac.Write([]byte(key))
|
||||
mac.Write([]byte(strconv.FormatInt(expiry, 10)))
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// VerifySignedURL prueft key/expiry/sig, wie sie z.B. aus den Query-
|
||||
// Parametern einer von SignedURL ausgestellten URL stammen (Pruefung 2:
|
||||
// abgelaufene URL wird abgewiesen). Die eigentliche HTTP-Auslieferung ist
|
||||
// nicht Bestandteil dieser Kachel (siehe DOC-01) — hier wird nur die
|
||||
// Signatur-/Ablauflogik bereitgestellt und getestet.
|
||||
func (d *LocalDriver) VerifySignedURL(key string, expiry int64, sig string) error {
|
||||
expected := d.sign(key, expiry)
|
||||
if subtle.ConstantTimeCompare([]byte(expected), []byte(sig)) != 1 {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
if time.Now().Unix() > expiry {
|
||||
return ErrURLExpired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user