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) }