add MinIO media URL refresh endpoint

Add POST /api/v1/files/refresh-url to issue fresh presigned URLs from object keys, minio:// references, or stale presigned URLs so clients can refresh media links before render.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-27 05:25:16 -07:00
parent 78f231f222
commit 0d02eb1a24
4 changed files with 101 additions and 0 deletions

View File

@ -14,6 +14,10 @@ type Client struct {
bucketName string
}
func (c *Client) BucketName() string {
return c.bucketName
}
func NewClient(endpoint, accessKey, secretKey string, useSSL bool, bucketName string) (*Client, error) {
mc, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),

View File

@ -66,6 +66,10 @@ func (s *Service) GetURL(ctx context.Context, objectKey string, expiry time.Dura
return s.client.GetFileURL(ctx, objectKey, expiry)
}
func (s *Service) BucketName() string {
return s.client.BucketName()
}
// Delete removes a file from MinIO.
func (s *Service) Delete(ctx context.Context, objectKey string) error {
s.logger.Info("Deleting file from MinIO", zap.String("object_key", objectKey))

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -31,6 +32,10 @@ type uploadMediaByURLReq struct {
Description string `json:"description"`
}
type refreshFileURLReq struct {
Reference string `json:"reference"`
}
// resolveFileURL converts a stored file path to a usable URL.
// If the path starts with "minio://", it generates a presigned URL.
// Otherwise it returns the path as-is (e.g. "/static/...").
@ -82,6 +87,93 @@ func (h *Handler) GetFileURL(c *fiber.Ctx) error {
})
}
func (h *Handler) extractObjectKeyFromReference(reference string) (string, error) {
ref := strings.TrimSpace(reference)
if ref == "" {
return "", fmt.Errorf("reference is required")
}
if strings.HasPrefix(ref, "minio://") {
key := strings.TrimPrefix(ref, "minio://")
if key == "" {
return "", fmt.Errorf("invalid minio reference")
}
return key, nil
}
u, err := url.Parse(ref)
if err == nil && u.Scheme != "" && u.Host != "" {
path := strings.TrimPrefix(u.Path, "/")
if path == "" {
return "", fmt.Errorf("invalid file URL")
}
bucket := strings.TrimSpace(h.minioSvc.BucketName())
if bucket != "" {
prefix := bucket + "/"
if strings.HasPrefix(path, prefix) {
path = strings.TrimPrefix(path, prefix)
}
}
if path == "" {
return "", fmt.Errorf("invalid file URL")
}
return path, nil
}
return ref, nil
}
// RefreshFileURL generates a new presigned URL from an object key, minio:// URI, or stale presigned URL.
// @Summary Refresh presigned URL for a file
// @Tags files
// @Accept json
// @Produce json
// @Param body body refreshFileURLReq true "reference (object key, minio://..., or existing presigned URL)"
// @Success 200 {object} domain.Response
// @Router /api/v1/files/refresh-url [post]
func (h *Handler) RefreshFileURL(c *fiber.Ctx) error {
if h.minioSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "File storage service is not available",
})
}
var req refreshFileURLReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
objectKey, err := h.extractObjectKeyFromReference(req.Reference)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid file reference",
Error: err.Error(),
})
}
freshURL, err := h.minioSvc.GetURL(c.Context(), objectKey, 1*time.Hour)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to refresh file URL",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "File URL refreshed",
Data: map[string]interface{}{
"object_key": objectKey,
"url": freshURL,
"expires_in": int((1 * time.Hour).Seconds()),
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// UploadMedia uploads an image/audio/video file and returns its URL and key.
// @Summary Upload media file
// @Tags files

View File

@ -113,6 +113,7 @@ func (a *App) initAppRoutes() {
// File storage (MinIO)
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
groupV1.Post("/files/refresh-url", a.authMiddleware, h.RefreshFileURL)
groupV1.Post("/files/upload", a.authMiddleware, h.UploadMedia)
groupV1.Post("/files/audio", a.authMiddleware, h.UploadAudio)
groupV1.Post("/questions/audio-answer", a.authMiddleware, h.SubmitAudioAnswer)