diff --git a/internal/pkgs/minio/client.go b/internal/pkgs/minio/client.go index 095e2a7..000ff32 100644 --- a/internal/pkgs/minio/client.go +++ b/internal/pkgs/minio/client.go @@ -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, ""), diff --git a/internal/services/minio/service.go b/internal/services/minio/service.go index 057130f..a053fa6 100644 --- a/internal/services/minio/service.go +++ b/internal/services/minio/service.go @@ -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)) diff --git a/internal/web_server/handlers/file_handler.go b/internal/web_server/handlers/file_handler.go index ca6e419..fa9e7bf 100644 --- a/internal/web_server/handlers/file_handler.go +++ b/internal/web_server/handlers/file_handler.go @@ -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 diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 5d8c6b4..6600c1d 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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)