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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
137 lines
4.3 KiB
Go
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)
|
|
}
|