Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
123 lines
4.5 KiB
Go
123 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"archivdms/internal/audit"
|
|
"archivdms/internal/storage"
|
|
)
|
|
|
|
// processingJobResponse ist die schlanke Sicht auf einen Queue-Job, die das
|
|
// Frontend für Statusbadge und Retry-Button braucht. Bewusst nicht der volle
|
|
// storage.ProcessingJob: interne Felder (derive_title, next_attempt_at,
|
|
// tenant_id) haben in der UI nichts zu suchen.
|
|
type processingJobResponse struct {
|
|
DocumentID int64 `json:"document_id"`
|
|
Status string `json:"status"`
|
|
RetryCount int `json:"retry_count"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
// handleGetProcessingJob liefert den Verarbeitungsstatus eines Dokuments
|
|
// (GET /api/documents/{id}/processing-job).
|
|
//
|
|
// ACL wie bei allen Dokument-Sub-Routen: erst GetDocument(id, tenantID) — der
|
|
// tenant_id-Filter dort ist der IDOR-Guard, ein fremdes Dokument liefert 404
|
|
// noch bevor irgendein Job gelesen wird.
|
|
//
|
|
// Hat ein Dokument keinen Job (kompletter Altbestand vor Einführung der
|
|
// Queue), wird KEIN 404 geliefert, sondern der processing_status des
|
|
// Dokuments selbst (Spalten-Default 'done'). Damit muss das Frontend keinen
|
|
// Sonderfall kennen: es bekommt immer einen Status.
|
|
func (s *Server) handleGetProcessingJob(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
|
|
}
|
|
tenantID := *sess.TenantID
|
|
|
|
doc, err := s.store.GetDocument(r.Context(), id, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrDocumentNotFound) {
|
|
writeError(w, http.StatusNotFound, "document not found")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "load document failed")
|
|
return
|
|
}
|
|
|
|
job, err := s.store.GetJobForDocument(r.Context(), id, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrNoJob) {
|
|
status := doc.ProcessingStatus
|
|
if status == "" {
|
|
status = storage.JobStatusDone
|
|
}
|
|
writeJSON(w, http.StatusOK, processingJobResponse{DocumentID: id, Status: status})
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "load processing job failed")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, processingJobResponse{
|
|
DocumentID: id,
|
|
Status: job.Status,
|
|
RetryCount: job.RetryCount,
|
|
ErrorMessage: job.ErrorMessage,
|
|
})
|
|
}
|
|
|
|
// handleRetryProcessingJob stellt einen dauerhaft fehlgeschlagenen Job manuell
|
|
// zurück in die Queue (POST /api/documents/{id}/processing-job/retry).
|
|
//
|
|
// Nur aus dem Status 'failed' heraus erlaubt — ein laufender oder bereits
|
|
// fertiger Job darf nicht zurückgesetzt werden (409), sonst könnte ein Klick
|
|
// eine gerade laufende Verarbeitung doppelt anstoßen. Der eigentliche Retry
|
|
// läuft danach ganz normal über den Dispatcher.
|
|
func (s *Server) handleRetryProcessingJob(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
|
|
}
|
|
tenantID := *sess.TenantID
|
|
|
|
if _, err := s.store.GetDocument(r.Context(), id, tenantID); err != nil {
|
|
if errors.Is(err, storage.ErrDocumentNotFound) {
|
|
writeError(w, http.StatusNotFound, "document not found")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "load document failed")
|
|
return
|
|
}
|
|
|
|
job, err := s.store.GetJobForDocument(r.Context(), id, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrNoJob) {
|
|
writeError(w, http.StatusNotFound, "no processing job for document")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "load processing job failed")
|
|
return
|
|
}
|
|
if job.Status != storage.JobStatusFailed {
|
|
writeError(w, http.StatusConflict, "processing job is not in failed state")
|
|
return
|
|
}
|
|
|
|
if err := s.store.RequeueJob(r.Context(), job.ID, tenantID); err != nil {
|
|
s.audlog.Log(audit.Entry{EventType: audit.EventDocumentProcessed, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: false, Detail: "manual retry failed: " + err.Error()})
|
|
writeError(w, http.StatusInternalServerError, "requeue failed")
|
|
return
|
|
}
|
|
s.audlog.Log(audit.Entry{EventType: audit.EventDocumentProcessed, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: true, Detail: "manual retry requeued job_id=" + strconv.FormatInt(job.ID, 10)})
|
|
|
|
writeJSON(w, http.StatusOK, processingJobResponse{DocumentID: id, Status: storage.JobStatusQueued})
|
|
}
|