Files
archivdms/internal/api/ocr_word_handlers.go
T
patrick 89de794356
CI / Backend (go vet, go test -cover) (push) Has been cancelled
CI / Frontend (ESLint, tsc, next build) (push) Has been cancelled
FDN-02/FDN-03/FDN-07/FDN-08: Migrations-Rollback, Objekt-Storage-Interface, go.sum-Fix, Observability
- FDN-02: Rollback-fähige Down-Migrationen (024-026), archivdms seed dev CLI
- FDN-03: internal/objectstore Interface + lokaler WORM-Treiber, signierte Download-URLs
- FDN-07: go.mod/go.sum vervollständigt (fehlender go-ldap/v3-Eintrag), CI-Pipeline (.gitea/workflows/ci.yml, bereits in FDN-01 committet) damit lauffähig
- FDN-08: Request-ID-Middleware, /metrics-Endpoint, Panic-Recovery, Login/Logout/Me technisches Logging inkl. Access-Log je Anfrage
2026-08-11 22:27:52 +02:00

76 lines
2.7 KiB
Go

package api
import (
"net/http"
"strconv"
)
// ocrWordResponse is the wire shape of one OCR word box. It is a dedicated DTO
// (not storage.OCRWord, which carries no json tags and exposes the internal row
// id / document_id) so the overlay renderer gets a compact, stable payload.
// Coordinates are in the original file's coordinate space — see
// internal/ocr/coords.go.
type ocrWordResponse struct {
Text string `json:"text"`
Left int `json:"left"`
Top int `json:"top"`
Width int `json:"width"`
Height int `json:"height"`
Confidence float64 `json:"confidence"`
Page int `json:"page"`
Block int `json:"block"`
Par int `json:"par"`
Line int `json:"line"`
}
// handleListDocumentOCRWords returns the stored word-level bounding boxes of a
// document (GET /api/documents/{id}/ocr-words), Phase 3 of the OCR
// text-highlight/overlay feature.
//
// Tenant/ACL: ocr_words has no tenant_id column, access is only ever mediated
// through document_id. The handler therefore performs the exact same ownership
// check as handleDocumentAuditLog / handleGetDocumentFile — GetDocument(id,
// tenantID) filters WHERE tenant_id and yields 404 for both "unknown id" and
// "foreign tenant", so the endpoint never reveals whether a document exists
// outside the caller's tenant — BEFORE any ocr_words row is read.
//
// Pure read: no audit entry, consistent with the other document GET handlers.
// Empty result serializes as [] (never null).
func (s *Server) handleListDocumentOCRWords(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "invalid document id")
return
}
// ACL/tenant check first — must precede the ocr_words lookup.
if _, err := s.store.GetDocument(r.Context(), id, *sess.TenantID); err != nil {
writeError(w, http.StatusNotFound, "document not found")
return
}
words, err := s.store.ListOCRWords(r.Context(), id)
if err != nil {
s.reqLog(r.Context()).Error("list ocr words failed", "document_id", id, "tenant_id", *sess.TenantID, "err", err)
writeError(w, http.StatusInternalServerError, "list ocr words failed")
return
}
out := make([]ocrWordResponse, 0, len(words))
for _, wd := range words {
out = append(out, ocrWordResponse{
Text: wd.Word,
Left: wd.Left,
Top: wd.Top,
Width: wd.Width,
Height: wd.Height,
Confidence: wd.Confidence,
Page: wd.Page,
Block: wd.Block,
Par: wd.Par,
Line: wd.Line,
})
}
writeJSON(w, http.StatusOK, out)
}