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>
155 lines
5.3 KiB
Go
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)
|
|
}
|