feat: Standbild-Upload bei der Pre-Publish-Prüfung
CLAUDE.md beschreibt die Prüfung seit dem ersten Commit als "Caption, Standbild und Vertragslage rein" — bisher wurde nur die Caption verarbeitet, das asset-Schema aus Migration 0001 blieb ungenutzt. - internal/store/asset.go: CreateAsset/GetLatestAssetForSubmission. Migration 0005 macht asset append-only (Trigger fehlte seit 0001, weil bis jetzt nichts hineinschrieb) — ein hochgeladenes Beweisstück wird nicht nachträglich ausgetauscht, aus demselben Grund wie bei extraction/finding/evidence_package. - handleCheck liest ein optionales "standbild"-Formularfeld (Bild- Upload, max. 8 MiB, Content-Type muss image/* sein), validiert es VOR dem Anlegen der Submission (ein ungültiger Upload hinterlässt so keine leere Beitrags-Zeile), speichert es danach unter ASSET_DIR und legt die Asset-Zeile an. - handleArchive bindet den Asset-Hash (falls vorhanden) in den Metadaten-Hash und ins PDF-Dossier ein (dossier.Data.AssetHash war bereits vorbereitet, wurde aber nie befüllt). - index.html: Formular auf multipart/form-data umgestellt (hx-encoding + enctype), neues optionales Dateifeld. handleCheck bleibt abwärtskompatibel zu urlencoded-Requests (ParseMultipartForm liefert ErrNotMultipart, das wird wie "kein Bild hochgeladen" behandelt, nicht wie ein Fehler). - ASSET_DIR neue Konfigurationsvariable (Default "assets", wie DOSSIER_DIR relativ zu WorkingDirectory=/var/lib/deklarix — kein postinst-Healing nötig, anders als bei RULES_DIR, dessen Default nicht zum installierten Pfad passt). Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen einen laufenden Server verifiziert (Upload, Hash in DB, Hash im erzeugten PDF via pdftotext, Ablehnung bei falschem Dateityp).
This commit is contained in:
65
internal/store/asset.go
Normal file
65
internal/store/asset.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// Asset ist eine zu einem Beitrag hochgeladene Datei (aktuell: das
|
||||
// Standbild/der Screenshot). Append-only wie extraction/finding/
|
||||
// evidence_package — ein hochgeladenes Beweisstück wird nicht
|
||||
// nachträglich ausgetauscht, siehe Migration.
|
||||
type Asset struct {
|
||||
ID string
|
||||
SubmissionID string
|
||||
Kind string
|
||||
Path string
|
||||
SHA256 string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// CreateAsset speichert ein Asset. sha256Hex ist der Hex-kodierte
|
||||
// SHA-256-Digest der Datei (siehe evidence.HashBytes) — dieselbe Form,
|
||||
// in der evidence_package.SHA256 seinen Hash speichert.
|
||||
func (s *Store) CreateAsset(ctx context.Context, submissionID, kind, path, sha256Hex string) (Asset, error) {
|
||||
var a Asset
|
||||
err := s.Pool.QueryRow(ctx, `
|
||||
INSERT INTO asset (submission_id, kind, path, sha256)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, submission_id, kind, path, sha256, created_at
|
||||
`, submissionID, kind, path, sha256Hex).Scan(
|
||||
&a.ID, &a.SubmissionID, &a.Kind, &a.Path, &a.SHA256, &a.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Asset{}, fmt.Errorf("store: create asset: %w", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// GetLatestAssetForSubmission liefert das zuletzt hochgeladene Asset
|
||||
// eines Beitrags. Liefert ErrNotFound, wenn kein Asset hochgeladen wurde
|
||||
// — das ist der Normalfall (ein Standbild ist optional), kein Fehler,
|
||||
// den Aufrufer wie einen echten Datenbankfehler behandeln sollten.
|
||||
func (s *Store) GetLatestAssetForSubmission(ctx context.Context, submissionID string) (Asset, error) {
|
||||
var a Asset
|
||||
err := s.Pool.QueryRow(ctx, `
|
||||
SELECT id, submission_id, kind, path, sha256, created_at
|
||||
FROM asset
|
||||
WHERE submission_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, submissionID).Scan(
|
||||
&a.ID, &a.SubmissionID, &a.Kind, &a.Path, &a.SHA256, &a.CreatedAt,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return Asset{}, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return Asset{}, fmt.Errorf("store: get latest asset: %w", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
Reference in New Issue
Block a user