Files
Aether/backend/internal/files/handler_trash.go
T
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:25:36 +02:00

145 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package files
import (
"errors"
"fmt"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"net/http"
"github.com/rs/zerolog/log"
"aether/internal/audit"
"aether/internal/auth"
"aether/pkg/response"
)
// Papierkorb: auflisten, wiederherstellen, endgültig entfernen.
//
// Gelöscht wird nie hart, sondern weich – erst von hier aus verschwinden die
// Bytes wirklich.
// GET /api/v1/trash
func (h *Handler) ListTrash(w http.ResponseWriter, r *http.Request) {
user := auth.UserFromContext(r.Context())
items, err := h.store.ListTrash(r.Context(), user.ID)
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
if items == nil {
items = []*TrashFile{}
}
response.JSON(w, http.StatusOK, items)
}
// POST /api/v1/trash/{id}/restore
func (h *Handler) RestoreFile(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r, "id")
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid file id")
return
}
owner, ok := h.resolveFile(w, r, id, true)
if !ok {
return
}
trashed, err := h.store.GetTrashedFile(r.Context(), id, owner.OwnerID)
if errors.Is(err, ErrNotFound) {
response.Error(w, http.StatusNotFound, "file not found in trash")
return
}
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
folderPath, err := h.store.GetFolderPath(r.Context(), trashed.FolderID)
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
newStorageKey := BuildStorageKey(owner.StorageRoot(), folderPath, trashed.Name)
if err := h.storage.Move(r.Context(), trashed.StorageKey, newStorageKey); err != nil {
response.Error(w, http.StatusInternalServerError, "storage error")
return
}
f, err := h.store.RestoreFile(r.Context(), id, owner.OwnerID, newStorageKey)
if errors.Is(err, ErrNotFound) {
response.Error(w, http.StatusNotFound, "file not found in trash")
return
}
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
h.logFile(r, audit.ActionFileRestore, id.String(), trashed.Name, "restored from trash")
response.JSON(w, http.StatusOK, f)
}
// DELETE /api/v1/trash/{id} – permanent delete
// DELETE /api/v1/trash – leert den Papierkorb vollständig.
func (h *Handler) EmptyTrash(w http.ResponseWriter, r *http.Request) {
user := auth.UserFromContext(r.Context())
fileIDs, keys, err := h.store.EmptyTrash(r.Context(), user.ID)
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
// Blobs erst nach dem Datenbank-Commit entfernen. Scheitert eine Löschung,
// bleibt höchstens eine Dateileiche zurück – das ist harmloser als ein
// Eintrag ohne Inhalt.
for _, key := range keys {
if err := h.storage.Delete(r.Context(), key); err != nil {
log.Ctx(r.Context()).Warn().Err(err).Str("key", key).
Msg("empty trash: blob not removed")
}
}
for _, id := range fileIDs {
_ = h.storage.Delete(r.Context(), "thumbs/"+id.String()+".jpg")
}
log.Ctx(r.Context()).Info().
Int("files", len(fileIDs)).Int("blobs", len(keys)).
Msg("trash emptied")
h.logFile(r, audit.ActionTrashEmpty, "", "",
fmt.Sprintf("%d files permanently deleted", len(fileIDs)))
response.NoContent(w)
}
func (h *Handler) HardDelete(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r, "id")
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid file id")
return
}
owner, ok := h.resolveFile(w, r, id, true)
if !ok {
return
}
storageKey, versionKeys, err := h.store.HardDeleteFile(r.Context(), id, owner.OwnerID)
if errors.Is(err, ErrNotFound) {
response.Error(w, http.StatusNotFound, "file not found in trash")
return
}
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
_ = h.storage.Delete(r.Context(), storageKey)
for _, key := range versionKeys {
_ = h.storage.Delete(r.Context(), key)
}
_ = h.storage.Delete(r.Context(), "thumbs/"+id.String()+".jpg")
h.logFile(r, audit.ActionHardDelete, id.String(), "", "permanently deleted from trash")
response.NoContent(w)
}