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).
66 lines
2.0 KiB
Go
66 lines
2.0 KiB
Go
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
|
|
}
|