upload.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. // Package handlers
  2. package handlers
  3. import (
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "time"
  9. "github.com/google/uuid"
  10. "github.com/minio/minio-go/v7"
  11. "github.com/minio/minio-go/v7/pkg/credentials"
  12. )
  13. const maxUploadSize = 10 << 20 // 10 MB
  14. type UploadHandler struct {
  15. minioClient *minio.Client
  16. bucket string
  17. endpoint string
  18. publicEndpoint string
  19. }
  20. func NewUploadHandler(endpoint, publicEndpoint, accessKey, secretKey, bucket string, useSSL bool) (*UploadHandler, error) {
  21. client, err := minio.New(endpoint, &minio.Options{
  22. Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
  23. Secure: useSSL,
  24. })
  25. if err != nil {
  26. return nil, err
  27. }
  28. return &UploadHandler{
  29. minioClient: client,
  30. bucket: bucket,
  31. endpoint: endpoint,
  32. publicEndpoint: publicEndpoint,
  33. }, nil
  34. }
  35. type presignedURLRequest struct {
  36. ContentType string `json:"content_type"`
  37. MaxSizeMB int `json:"max_size_mb"`
  38. }
  39. type presignedURLResponse struct {
  40. UploadURL string `json:"upload_url"`
  41. FileURL string `json:"file_url"`
  42. Fields map[string]string `json:"fields"`
  43. }
  44. func (h *UploadHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
  45. r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
  46. if err := r.ParseMultipartForm(maxUploadSize); err != nil {
  47. writeError(w, http.StatusBadRequest, "file too large or invalid multipart", err)
  48. return
  49. }
  50. file, header, err := r.FormFile("file")
  51. if err != nil {
  52. writeError(w, http.StatusBadRequest, "file is required", err)
  53. return
  54. }
  55. defer file.Close()
  56. contentType := header.Header.Get("Content-Type")
  57. validContentTypes := map[string]bool{
  58. "image/jpeg": true, "image/png": true, "image/webp": true, "image/heic": true,
  59. }
  60. if !validContentTypes[contentType] {
  61. writeError(w, http.StatusBadRequest, "unsupported content type", nil)
  62. return
  63. }
  64. ext := ".jpg"
  65. switch contentType {
  66. case "image/png":
  67. ext = ".png"
  68. case "image/webp":
  69. ext = ".webp"
  70. case "image/heic":
  71. ext = ".heic"
  72. }
  73. objectName := "uploads/" + uuid.New().String() + ext
  74. _, err = h.minioClient.PutObject(r.Context(), h.bucket, objectName, file, header.Size,
  75. minio.PutObjectOptions{ContentType: contentType},
  76. )
  77. if err != nil {
  78. writeError(w, http.StatusInternalServerError, "failed to upload file", err)
  79. return
  80. }
  81. fileURL := fmt.Sprintf("%s/%s/%s", h.publicEndpoint, h.bucket, objectName)
  82. writeJSON(w, http.StatusOK, map[string]string{
  83. "file_url": fileURL,
  84. })
  85. }
  86. func (h *UploadHandler) PresignedURL(w http.ResponseWriter, r *http.Request) {
  87. var req presignedURLRequest
  88. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  89. writeError(w, http.StatusBadRequest, "invalid request body", err)
  90. return
  91. }
  92. if req.MaxSizeMB <= 0 || req.MaxSizeMB > 50 {
  93. req.MaxSizeMB = 10
  94. }
  95. validContentTypes := map[string]bool{
  96. "image/jpeg": true, "image/png": true, "image/webp": true, "image/heic": true,
  97. }
  98. if !validContentTypes[req.ContentType] {
  99. writeError(w, http.StatusBadRequest, "unsupported content type", nil)
  100. return
  101. }
  102. fileID := uuid.New().String()
  103. ext := ".jpg"
  104. switch req.ContentType {
  105. case "image/png":
  106. ext = ".png"
  107. case "image/webp":
  108. ext = ".webp"
  109. case "image/heic":
  110. ext = ".heic"
  111. }
  112. objectName := "uploads/" + fileID + ext
  113. policy := minio.NewPostPolicy()
  114. policy.SetBucket(h.bucket)
  115. policy.SetKey(objectName)
  116. policy.SetExpires(time.Now().Add(15 * time.Minute))
  117. policy.SetContentType(req.ContentType)
  118. policy.SetContentLengthRange(1, int64(req.MaxSizeMB)*1024*1024)
  119. uploadURL, formData, err := h.minioClient.PresignedPostPolicy(r.Context(), policy)
  120. if err != nil {
  121. writeError(w, http.StatusInternalServerError, "failed to generate upload URL", err)
  122. return
  123. }
  124. // Replace internal endpoint (minio:9000) with public endpoint for browser uploads
  125. publicUploadURL := uploadURL
  126. uploadURLStr := h.publicEndpoint + uploadURL.Path + "?" + uploadURL.RawQuery
  127. if parsed, err := url.Parse(uploadURLStr); err == nil {
  128. publicUploadURL = parsed
  129. }
  130. fileURL := h.publicEndpoint + "/" + h.bucket + "/" + objectName
  131. writeJSON(w, http.StatusOK, presignedURLResponse{
  132. UploadURL: publicUploadURL.String(),
  133. FileURL: fileURL,
  134. Fields: formData,
  135. })
  136. }