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>
145 lines
4.0 KiB
Go
145 lines
4.0 KiB
Go
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)
|
||
}
|