feat(storage): serve media through backend and add local FS backend
- introduce storage.Backend with S3 and local file-system implementations - deliver all blobs via signed GET/HEAD /api/objects/<key> (Range, Content-Type) - stream S3 objects through the backend; remove presigned URL path - share shared HMAC Signer between backends - remove S3_PUBLIC_URL; rename S3_PRESIGN_TTL to OBJECT_URL_TTL - make storage label in health dynamic; nginx proxy_buffering off for objects
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
pool *pgxpool.Pool
|
||||
storage *storage.Client
|
||||
storage storage.Backend
|
||||
hub *sse.Hub
|
||||
|
||||
albumsStore *albums.Store
|
||||
@@ -47,7 +48,7 @@ type Server struct {
|
||||
share *sharelinks.Service
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, pool *pgxpool.Pool, st *storage.Client, hub *sse.Hub) *Server {
|
||||
func New(cfg *config.Config, pool *pgxpool.Pool, st storage.Backend, hub *sse.Hub) *Server {
|
||||
authStore := auth.NewStore(pool)
|
||||
tm := auth.NewTokenManager(cfg.Auth.JWTSecret, cfg.Auth.AccessTTL, cfg.Auth.RefreshTTL)
|
||||
authSvc := auth.NewService(authStore, tm)
|
||||
@@ -109,6 +110,8 @@ func (s *Server) Router() *gin.Engine {
|
||||
|
||||
api := r.Group("/api")
|
||||
api.GET("/health", s.health)
|
||||
api.GET("/objects/*key", s.serveObject)
|
||||
api.HEAD("/objects/*key", s.serveObject)
|
||||
|
||||
api.POST("/auth/register", authHandler.Register)
|
||||
api.POST("/auth/login", authHandler.Login)
|
||||
@@ -183,17 +186,37 @@ func (s *Server) health(c *gin.Context) {
|
||||
if err := s.pool.Ping(ctx); err != nil {
|
||||
dbStatus = "error"
|
||||
}
|
||||
s3Status := "ok"
|
||||
storageLabel := s.cfg.Storage.Backend
|
||||
if storageLabel == "" {
|
||||
storageLabel = "s3"
|
||||
}
|
||||
storeStatus := "ok"
|
||||
if err := s.storage.Ping(ctx); err != nil {
|
||||
s3Status = "error"
|
||||
storeStatus = "error"
|
||||
}
|
||||
status := http.StatusOK
|
||||
overall := "ok"
|
||||
if dbStatus != "ok" || s3Status != "ok" {
|
||||
if dbStatus != "ok" || storeStatus != "ok" {
|
||||
status = http.StatusServiceUnavailable
|
||||
overall = "degraded"
|
||||
}
|
||||
c.JSON(status, gin.H{"status": overall, "db": dbStatus, "s3": s3Status})
|
||||
c.JSON(status, gin.H{"status": overall, "db": dbStatus, storageLabel: storeStatus})
|
||||
}
|
||||
|
||||
func (s *Server) serveObject(c *gin.Context) {
|
||||
key := strings.TrimPrefix(c.Param("key"), "/")
|
||||
switch err := s.storage.ServeObject(c.Writer, c.Request, key); {
|
||||
case err == nil:
|
||||
return
|
||||
case errors.Is(err, storage.ErrSignedURLInvalid):
|
||||
c.Status(http.StatusForbidden)
|
||||
case errors.Is(err, storage.ErrObjectNotFound):
|
||||
httpx.RespondError(c, httpx.NotFound("object_not_found", "object not found"))
|
||||
case errors.Is(err, storage.ErrRangeNotSatisfiable):
|
||||
c.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
default:
|
||||
c.Status(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) albumSSE(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user