Files
nexarch/dms/internal/trashapi/handler.go
T
patrickandClaude Sonnet 5 19776bdc24 DOC-12: papierkorb-oberfläche
trashapi (Liste mit Restlaufzeit, Massen-/Einzelwiederherstellung,
sofortige endgueltige Loeschung) als Backend-Voraussetzung, Papierkorb-Seite
im dms-browser mit Bestaetigungsdialog vor jeder endgueltigen Loeschung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 00:04:29 +02:00

155 lines
5.3 KiB
Go

// Package trashapi implementiert DOC-12s Backend-Voraussetzung: HTTP-
// Endpunkte für Papierkorb-Liste, Wiederherstellung (einzeln und in
// Auswahl) und endgültiges Löschen. Ruft ausschließlich internal/trash auf,
// keine eigene Papierkorb-Logik.
package trashapi
import (
"encoding/json"
"net/http"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/storage"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/trash"
)
type Server struct {
pool *pgxpool.Pool
storageSvc *storage.Service
retentionDuration time.Duration
}
// NewServer nimmt die Papierkorb-Frist als Parameter entgegen — der Aufrufer
// (cmd/app) liest sie aus einer Umgebungsvariable, dieses Paket kennt keine
// eigene Konfigurationsquelle.
func NewServer(pool *pgxpool.Pool, storageSvc *storage.Service, retentionDuration time.Duration) *Server {
return &Server{pool: pool, storageSvc: storageSvc, retentionDuration: retentionDuration}
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /trash", s.handleList)
mux.HandleFunc("POST /trash/restore", s.handleRestore)
mux.HandleFunc("POST /trash/purge", s.handlePurge)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
type entryDTO struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
TrashedAt string `json:"trashed_at"`
RemainingSeconds int64 `json:"remaining_seconds"`
}
// handleList liefert alle Papierkorb-Einträge mit Restlaufzeit
// (Akzeptanzkriterium 1). Eine bereits abgelaufene Restlaufzeit wird als 0
// ausgegeben, nie negativ — der Aufräum-Job (DOC-11) entfernt solche
// Einträge ohnehin zeitnah, ein negativer Wert wäre nur verwirrend.
func (s *Server) handleList(w http.ResponseWriter, r *http.Request) {
entries, err := trash.ListTrashed(r.Context(), s.pool)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
dtos := make([]entryDTO, len(entries))
for i, e := range entries {
remaining := s.retentionDuration - time.Since(e.TrashedAt)
remainingSeconds := int64(remaining.Seconds())
if remainingSeconds < 0 {
remainingSeconds = 0
}
dtos[i] = entryDTO{
ID: e.ID, Type: string(e.Type), Name: e.Name,
TrashedAt: e.TrashedAt.UTC().Format(time.RFC3339), RemainingSeconds: remainingSeconds,
}
}
writeJSON(w, dtos)
}
type selectionRequest struct {
DocumentIDs []string `json:"document_ids"`
FolderIDs []string `json:"folder_ids"`
}
type selectionResult struct {
RestoredDocumentIDs []string `json:"restored_document_ids,omitempty"`
RestoredFolderIDs []string `json:"restored_folder_ids,omitempty"`
PurgedDocumentIDs []string `json:"purged_document_ids,omitempty"`
PurgedFolderIDs []string `json:"purged_folder_ids,omitempty"`
Failed map[string]string `json:"failed,omitempty"`
}
// handleRestore stellt eine Auswahl von Dokumenten UND Ordnern wieder her
// (Akzeptanzkriterium 2: einzeln — Auswahl mit einem Element — UND in
// Auswahl gleichermaßen über denselben Endpunkt).
func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
var req selectionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error())
return
}
result := selectionResult{Failed: map[string]string{}}
for _, id := range req.DocumentIDs {
if err := trash.RestoreDocument(r.Context(), s.pool, id); err != nil {
result.Failed[id] = err.Error()
continue
}
result.RestoredDocumentIDs = append(result.RestoredDocumentIDs, id)
}
for _, id := range req.FolderIDs {
if err := trash.RestoreFolder(r.Context(), s.pool, id); err != nil {
result.Failed[id] = err.Error()
continue
}
result.RestoredFolderIDs = append(result.RestoredFolderIDs, id)
}
if len(result.Failed) == 0 {
result.Failed = nil
}
writeJSON(w, result)
}
// handlePurge löscht eine Auswahl von Dokumenten UND Ordnern sofort
// endgültig (Akzeptanzkriterium 3) — die Bestätigung selbst liegt in der
// Oberfläche (Dialog vor diesem Aufruf), dieser Endpunkt führt nur aus.
func (s *Server) handlePurge(w http.ResponseWriter, r *http.Request) {
var req selectionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error())
return
}
result := selectionResult{Failed: map[string]string{}}
for _, id := range req.DocumentIDs {
if err := trash.PurgeDocumentNow(r.Context(), s.pool, s.storageSvc, id); err != nil {
result.Failed[id] = err.Error()
continue
}
result.PurgedDocumentIDs = append(result.PurgedDocumentIDs, id)
}
for _, id := range req.FolderIDs {
if err := trash.PurgeFolderNow(r.Context(), s.pool, id); err != nil {
result.Failed[id] = err.Error()
continue
}
result.PurgedFolderIDs = append(result.PurgedFolderIDs, id)
}
if len(result.Failed) == 0 {
result.Failed = nil
}
writeJSON(w, result)
}