// Package handlers package handlers import ( "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) const maxUploadSize = 10 << 20 // 10 MB type UploadHandler struct { minioClient *minio.Client bucket string endpoint string publicEndpoint string } func NewUploadHandler(endpoint, publicEndpoint, accessKey, secretKey, bucket string, useSSL bool) (*UploadHandler, error) { client, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: useSSL, }) if err != nil { return nil, err } return &UploadHandler{ minioClient: client, bucket: bucket, endpoint: endpoint, publicEndpoint: publicEndpoint, }, nil } type presignedURLRequest struct { ContentType string `json:"content_type"` MaxSizeMB int `json:"max_size_mb"` } type presignedURLResponse struct { UploadURL string `json:"upload_url"` FileURL string `json:"file_url"` Fields map[string]string `json:"fields"` } func (h *UploadHandler) UploadFile(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) if err := r.ParseMultipartForm(maxUploadSize); err != nil { writeError(w, http.StatusBadRequest, "file too large or invalid multipart", err) return } file, header, err := r.FormFile("file") if err != nil { writeError(w, http.StatusBadRequest, "file is required", err) return } defer file.Close() contentType := header.Header.Get("Content-Type") validContentTypes := map[string]bool{ "image/jpeg": true, "image/png": true, "image/webp": true, "image/heic": true, } if !validContentTypes[contentType] { writeError(w, http.StatusBadRequest, "unsupported content type", nil) return } ext := ".jpg" switch contentType { case "image/png": ext = ".png" case "image/webp": ext = ".webp" case "image/heic": ext = ".heic" } objectName := "uploads/" + uuid.New().String() + ext _, err = h.minioClient.PutObject(r.Context(), h.bucket, objectName, file, header.Size, minio.PutObjectOptions{ContentType: contentType}, ) if err != nil { writeError(w, http.StatusInternalServerError, "failed to upload file", err) return } fileURL := fmt.Sprintf("%s/%s/%s", h.publicEndpoint, h.bucket, objectName) writeJSON(w, http.StatusOK, map[string]string{ "file_url": fileURL, }) } func (h *UploadHandler) PresignedURL(w http.ResponseWriter, r *http.Request) { var req presignedURLRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body", err) return } if req.MaxSizeMB <= 0 || req.MaxSizeMB > 50 { req.MaxSizeMB = 10 } validContentTypes := map[string]bool{ "image/jpeg": true, "image/png": true, "image/webp": true, "image/heic": true, } if !validContentTypes[req.ContentType] { writeError(w, http.StatusBadRequest, "unsupported content type", nil) return } fileID := uuid.New().String() ext := ".jpg" switch req.ContentType { case "image/png": ext = ".png" case "image/webp": ext = ".webp" case "image/heic": ext = ".heic" } objectName := "uploads/" + fileID + ext policy := minio.NewPostPolicy() policy.SetBucket(h.bucket) policy.SetKey(objectName) policy.SetExpires(time.Now().Add(15 * time.Minute)) policy.SetContentType(req.ContentType) policy.SetContentLengthRange(1, int64(req.MaxSizeMB)*1024*1024) uploadURL, formData, err := h.minioClient.PresignedPostPolicy(r.Context(), policy) if err != nil { writeError(w, http.StatusInternalServerError, "failed to generate upload URL", err) return } // Replace internal endpoint (minio:9000) with public endpoint for browser uploads publicUploadURL := uploadURL uploadURLStr := h.publicEndpoint + uploadURL.Path + "?" + uploadURL.RawQuery if parsed, err := url.Parse(uploadURLStr); err == nil { publicUploadURL = parsed } fileURL := h.publicEndpoint + "/" + h.bucket + "/" + objectName writeJSON(w, http.StatusOK, presignedURLResponse{ UploadURL: publicUploadURL.String(), FileURL: fileURL, Fields: formData, }) }