package restapi
import (
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/danielmiessler/fabric/internal/plugins/db"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
"github.com/gin-gonic/gin"
)
type StorageHandler[T any] struct {
storage db.Storage[T]
}
func setHSTS(c *gin.Context) {
c.Writer.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
func storageError(c *gin.Context, err error) {
if _, ok := errors.AsType[*fsdb.InvalidStorageNameError](err); ok {
setHSTS(c)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
slog.Error("storage operation failed", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
}
func rejectInvalidStorageName(c *gin.Context, name string) bool {
if name == "" {
return false
}
if err := fsdb.ValidateStorageName(name); err != nil {
setHSTS(c)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return true
}
return false
}
func NewStorageHandler[T any](r *gin.Engine, entityType string, storage db.Storage[T]) (ret *StorageHandler[T]) {
ret = &StorageHandler[T]{storage: storage}
r.GET(fmt.Sprintf("/%s/:name", entityType), ret.Get)
r.GET(fmt.Sprintf("/%s/names", entityType), ret.GetNames)
r.DELETE(fmt.Sprintf("/%s/:name", entityType), auditChange, ret.Delete)
r.GET(fmt.Sprintf("/%s/exists/:name", entityType), ret.Exists)
r.PUT(fmt.Sprintf("/%s/rename/:oldName/:newName", entityType), auditChange, ret.Rename)
r.POST(fmt.Sprintf("/%s/:name", entityType), auditChange, ret.Save)
return
}
func (h *StorageHandler[T]) Get(c *gin.Context) {
name := c.Param("name")
item, err := h.storage.Get(name)
if err != nil {
storageError(c, err)
return
}
c.JSON(http.StatusOK, item)
}
func (h *StorageHandler[T]) GetNames(c *gin.Context) {
names, err := h.storage.GetNames()
if err != nil {
storageError(c, err)
return
}
c.JSON(http.StatusOK, names)
}
func (h *StorageHandler[T]) Delete(c *gin.Context) {
name := c.Param("name")
err := h.storage.Delete(name)
if err != nil {
storageError(c, err)
return
}
c.Status(http.StatusOK)
}
func (h *StorageHandler[T]) Exists(c *gin.Context) {
name := c.Param("name")
if rejectInvalidStorageName(c, name) {
return
}
exists := h.storage.Exists(name)
c.JSON(http.StatusOK, exists)
}
func (h *StorageHandler[T]) Rename(c *gin.Context) {
oldName := c.Param("oldName")
newName := c.Param("newName")
err := h.storage.Rename(oldName, newName)
if err != nil {
storageError(c, err)
return
}
c.Status(http.StatusOK)
}
func (h *StorageHandler[T]) Save(c *gin.Context) {
name := c.Param("name")
body := c.Request.Body
defer body.Close()
content, err := io.ReadAll(body)
if err != nil {
storageError(c, err)
return
}
err = h.storage.Save(name, content)
if err != nil {
storageError(c, err)
return
}
c.Status(http.StatusOK)
}