Files
nexarch/dms/internal/uploadapi/handler.go
T
patrickandClaude Sonnet 5 2acefefd8d
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
QA-06: Groessenvalidierung, 409 bei unvollstaendigem Upload, Testkorrektur
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 09:37:49 +02:00

137 lines
4.3 KiB
Go

// Package uploadapi implementiert DOC-03s Backend-Voraussetzung: DOC-01
// liefert bislang nur einen Go-Service (internal/upload), keinen
// HTTP-Endpunkt — dieselbe Lücke wie bei DOC-05/documentapi. Die
// Drag-and-Drop-Oberfläche kann Uploads nur über die öffentliche
// HTTP-Schnittstelle ansprechen, nicht über Go-Funktionsaufrufe.
package uploadapi
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/upload"
)
type Server struct {
svc *upload.Service
}
func NewServer(svc *upload.Service) *Server {
return &Server{svc: svc}
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /uploads", s.handleStart)
mux.HandleFunc("PUT /uploads/{id}/chunk", s.handleChunk)
mux.HandleFunc("GET /uploads/{id}", s.handleStatus)
mux.HandleFunc("POST /uploads/{id}/complete", s.handleComplete)
}
type startRequest struct {
Filename string `json:"filename"`
MimeType string `json:"mimeType"`
TotalSize int64 `json:"totalSize"`
FolderID *string `json:"folderId,omitempty"`
}
type startResponse struct {
SessionID string `json:"sessionId"`
}
func (s *Server) handleStart(w http.ResponseWriter, r *http.Request) {
var req startRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungueltiger anfrage-body", http.StatusBadRequest)
return
}
createdBy := r.Header.Get("X-User-Id")
sessionID, err := s.svc.StartUpload(r.Context(), req.Filename, req.MimeType, req.TotalSize, req.FolderID, createdBy)
if err != nil {
writeUploadError(w, err)
return
}
writeJSON(w, http.StatusCreated, startResponse{SessionID: sessionID})
}
type chunkResponse struct {
BytesReceived int64 `json:"bytesReceived"`
}
func (s *Server) handleChunk(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
offset, err := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64)
if err != nil || offset < 0 {
http.Error(w, "ungueltiger offset-parameter", http.StatusBadRequest)
return
}
bytesReceived, err := s.svc.UploadChunk(r.Context(), sessionID, offset, r.Body)
if err != nil {
writeUploadError(w, err)
return
}
writeJSON(w, http.StatusOK, chunkResponse{BytesReceived: bytesReceived})
}
type statusResponse struct {
SessionID string `json:"sessionId"`
Filename string `json:"filename"`
TotalSize int64 `json:"totalSize"`
BytesReceived int64 `json:"bytesReceived"`
Status string `json:"status"`
DocumentID *string `json:"documentId,omitempty"`
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
sess, err := s.svc.Status(r.Context(), sessionID)
if err != nil {
writeUploadError(w, err)
return
}
writeJSON(w, http.StatusOK, statusResponse{
SessionID: sess.ID, Filename: sess.Filename, TotalSize: sess.TotalSize,
BytesReceived: sess.BytesReceived, Status: sess.Status, DocumentID: sess.DocumentID,
})
}
type completeResponse struct {
DocumentID string `json:"documentId"`
RevisionID string `json:"revisionId"`
}
func (s *Server) handleComplete(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("id")
documentID, revisionID, err := s.svc.Complete(r.Context(), sessionID, r.URL.Query().Get("confirm_duplicate") == "true")
if err != nil {
var dup *upload.DuplicateError
if errors.As(err, &dup) {
writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error(), "documentId": dup.DocumentID, "revisionId": dup.RevisionID})
return
}
writeUploadError(w, err)
return
}
writeJSON(w, http.StatusOK, completeResponse{DocumentID: documentID, RevisionID: revisionID})
}
func writeUploadError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, upload.ErrSessionNotFound):
http.Error(w, err.Error(), http.StatusNotFound)
case errors.Is(err, upload.ErrFileTooLarge), errors.Is(err, upload.ErrDisallowedMimeType), errors.Is(err, upload.ErrInvalidOffset), errors.Is(err, upload.ErrInvalidSize):
http.Error(w, err.Error(), http.StatusBadRequest)
case errors.Is(err, upload.ErrIncompleteUpload):
http.Error(w, err.Error(), http.StatusConflict)
default:
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}