package handlers import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "path/filepath" "strings" "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // MediaHandler uploads media to Cloudflare R2. If s3Client is provided, it uses // the S3-compatible API. Otherwise, it falls back to Cloudflare R2 HTTP API // using account ID + API token from the config. type MediaHandler struct { s3Client *s3.Client useS3 bool accountID string apiToken string bucket string videoBucket string publicDomain string videoDomain string } func NewMediaHandler(s3Client *s3.Client, accountID string, apiToken string, bucket string, videoBucket string, publicDomain string, videoDomain string) *MediaHandler { return &MediaHandler{ s3Client: s3Client, useS3: s3Client != nil, accountID: accountID, apiToken: apiToken, bucket: bucket, videoBucket: videoBucket, publicDomain: publicDomain, videoDomain: videoDomain, } } func (h *MediaHandler) Upload(c *gin.Context) { fileHeader, err := c.FormFile("media") if err != nil { fileHeader, err = c.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No media file found"}) return } } mediaType := c.PostForm("type") if mediaType == "" { if strings.HasPrefix(fileHeader.Header.Get("Content-Type"), "video/") { mediaType = "video" } else { mediaType = "image" } } ext := filepath.Ext(fileHeader.Filename) if ext == "" { if mediaType == "video" { ext = ".mp4" } else { ext = ".jpg" } } userID := c.GetString("user_id") if userID == "" { userID = "anon" } objectKey := fmt.Sprintf("uploads/%s/%s%s", userID, uuid.New().String(), ext) targetBucket := h.bucket targetDomain := h.publicDomain if mediaType == "video" { if h.videoBucket != "" { targetBucket = h.videoBucket } if h.videoDomain != "" { targetDomain = h.videoDomain } } var publicURL string if h.useS3 { publicURL, err = h.putObjectS3(c, fileHeader, targetBucket, objectKey, targetDomain) } else { publicURL, err = h.putObjectR2API(c, fileHeader, targetBucket, objectKey, targetDomain) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to upload media: %v", err)}) return } c.JSON(http.StatusOK, gin.H{ "url": publicURL, "publicUrl": publicURL, "signedUrl": publicURL, "fileName": objectKey, "file_name": objectKey, "fileSize": fileHeader.Size, "file_size": fileHeader.Size, "type": mediaType, }) } func (h *MediaHandler) putObjectS3(c *gin.Context, fileHeader *multipart.FileHeader, bucket string, key string, publicDomain string) (string, error) { src, err := fileHeader.Open() if err != nil { return "", err } defer src.Close() ctx := c.Request.Context() _, err = h.s3Client.PutObject(ctx, &s3.PutObjectInput{ Bucket: &bucket, Key: &key, Body: src, ContentType: &[]string{fileHeader.Header.Get("Content-Type")}[0], }) if err != nil { return "", err } if publicDomain != "" { return fmt.Sprintf("https://%s/%s", publicDomain, key), nil } // Fallback to path (relative); AssetService can sign it later. return key, nil } func (h *MediaHandler) putObjectR2API(c *gin.Context, fileHeader *multipart.FileHeader, bucket string, key string, publicDomain string) (string, error) { if h.accountID == "" || h.apiToken == "" { return "", fmt.Errorf("R2 API credentials missing") } src, err := fileHeader.Open() if err != nil { return "", err } defer src.Close() fileBytes, err := io.ReadAll(src) if err != nil { return "", err } endpoint := fmt.Sprintf("https://api.cloudflare.com/client/v4/accounts/%s/r2/buckets/%s/objects/%s", h.accountID, bucket, key) req, err := http.NewRequestWithContext(c.Request.Context(), "PUT", endpoint, bytes.NewReader(fileBytes)) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+h.apiToken) req.Header.Set("Content-Type", fileHeader.Header.Get("Content-Type")) client := &http.Client{Timeout: 60 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { return "", fmt.Errorf("R2 upload failed (%d): %s", resp.StatusCode, string(body)) } if publicDomain != "" { return fmt.Sprintf("https://%s/%s", publicDomain, key), nil } return fmt.Sprintf("https://%s.r2.cloudflarestorage.com/%s/%s", h.accountID, bucket, key), nil }