• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cshum / imagor / 18090192958

29 Sep 2025 08:06AM UTC coverage: 91.972% (-0.6%) from 92.565%
18090192958

Pull #617

github

cshum
handlePostRequest
Pull Request #617: feat: POST Upload Endpoint and Upload Form

220 of 273 new or added lines in 6 files covered. (80.59%)

1 existing line in 1 file now uncovered.

5270 of 5730 relevant lines covered (91.97%)

1.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.35
/loader/uploadloader/uploadloader.go
1
package uploadloader
2

3
import (
4
        "fmt"
5
        "io"
6
        "mime"
7
        "net/http"
8
        "strings"
9

10
        "github.com/cshum/imagor"
11
)
12

13
// UploadLoader handles POST request uploads and implements imagor.Loader interface
14
type UploadLoader struct {
15
        // MaxAllowedSize maximum bytes allowed for uploaded image
16
        MaxAllowedSize int
17

18
        // Accept set accepted Content-Type for uploads
19
        Accept string
20

21
        // FormFieldName the form field name to extract file from multipart uploads
22
        FormFieldName string
23

24
        accepts []string
25
}
26

27
// New creates UploadLoader
28
func New(options ...Option) *UploadLoader {
1✔
29
        u := &UploadLoader{
1✔
30
                MaxAllowedSize: 32 << 20, // 32MB default
1✔
31
                Accept:         "image/*",
1✔
32
                FormFieldName:  "image",
1✔
33
        }
1✔
34

1✔
35
        for _, option := range options {
2✔
36
                option(u)
1✔
37
        }
1✔
38

39
        if u.Accept != "" {
2✔
40
                for _, seg := range strings.Split(u.Accept, ",") {
2✔
41
                        if typ := parseContentType(seg); typ != "" {
2✔
42
                                u.accepts = append(u.accepts, typ)
1✔
43
                        }
1✔
44
                }
45
        }
46

47
        return u
1✔
48
}
49

50
// Get implements imagor.Loader interface for POST uploads
51
func (u *UploadLoader) Get(r *http.Request, key string) (*imagor.Blob, error) {
1✔
52
        // Only handle POST requests
1✔
53
        if r.Method != http.MethodPost {
2✔
54
                return nil, imagor.ErrNotFound
1✔
55
        }
1✔
56

57
        // For uploads, key should be empty - if not empty, it's invalid
58
        // This is inverse to httploader where key should not be empty
59
        if key != "" {
2✔
60
                return nil, imagor.ErrInvalid
1✔
61
        }
1✔
62

63
        contentType := r.Header.Get("Content-Type")
1✔
64
        if contentType == "" {
2✔
65
                return nil, imagor.NewError("missing Content-Type header", http.StatusBadRequest)
1✔
66
        }
1✔
67

68
        // Check if it's multipart form data
69
        if strings.HasPrefix(contentType, "multipart/form-data") {
2✔
70
                return u.handleMultipartUpload(r)
1✔
71
        }
1✔
72

73
        // Handle raw body upload
74
        return u.handleRawUpload(r)
1✔
75
}
76

77
func (u *UploadLoader) handleMultipartUpload(r *http.Request) (*imagor.Blob, error) {
1✔
78
        // Parse multipart form with size limit
1✔
79
        err := r.ParseMultipartForm(int64(u.MaxAllowedSize))
1✔
80
        if err != nil {
1✔
NEW
81
                return nil, imagor.NewError(
×
NEW
82
                        fmt.Sprintf("failed to parse multipart form: %v", err),
×
NEW
83
                        http.StatusBadRequest)
×
NEW
84
        }
×
85

86
        file, header, err := r.FormFile(u.FormFieldName)
1✔
87
        if err != nil {
2✔
88
                return nil, imagor.NewError(
1✔
89
                        fmt.Sprintf("failed to get form file '%s': %v", u.FormFieldName, err),
1✔
90
                        http.StatusBadRequest)
1✔
91
        }
1✔
92

93
        // Check file size
94
        if header.Size > int64(u.MaxAllowedSize) {
1✔
NEW
95
                return nil, imagor.ErrMaxSizeExceeded
×
NEW
96
        }
×
97

98
        // Validate content type if specified
99
        if !u.validateContentType(header.Header.Get("Content-Type")) {
1✔
NEW
100
                return nil, imagor.ErrUnsupportedFormat
×
NEW
101
        }
×
102

103
        return u.createBlobFromReader(file, header.Size, header.Header.Get("Content-Type"))
1✔
104
}
105

106
func (u *UploadLoader) handleRawUpload(r *http.Request) (*imagor.Blob, error) {
1✔
107
        contentType := r.Header.Get("Content-Type")
1✔
108

1✔
109
        // Validate content type
1✔
110
        if !u.validateContentType(contentType) {
2✔
111
                return nil, imagor.ErrUnsupportedFormat
1✔
112
        }
1✔
113

114
        // Check content length
115
        contentLength := r.ContentLength
1✔
116
        if contentLength > int64(u.MaxAllowedSize) {
2✔
117
                return nil, imagor.ErrMaxSizeExceeded
1✔
118
        }
1✔
119

120
        // Limit reader to max allowed size as additional safety
121
        limitedReader := io.LimitReader(r.Body, int64(u.MaxAllowedSize)+1)
1✔
122

1✔
123
        return u.createBlobFromReader(limitedReader, contentLength, contentType)
1✔
124
}
125

126
func (u *UploadLoader) createBlobFromReader(reader io.Reader, size int64, contentType string) (*imagor.Blob, error) {
1✔
127
        blob := imagor.NewBlob(func() (io.ReadCloser, int64, error) {
2✔
128
                // For uploads, we need to read the data once and store it
1✔
129
                // since the request body can only be read once
1✔
130
                data, err := io.ReadAll(reader)
1✔
131
                if err != nil {
1✔
NEW
132
                        return nil, 0, err
×
NEW
133
                }
×
134

135
                // Check if we exceeded the size limit during reading
136
                if len(data) > u.MaxAllowedSize {
1✔
NEW
137
                        return nil, 0, imagor.ErrMaxSizeExceeded
×
NEW
138
                }
×
139

140
                actualSize := int64(len(data))
1✔
141
                return io.NopCloser(strings.NewReader(string(data))), actualSize, nil
1✔
142
        })
143

144
        if contentType != "" {
2✔
145
                blob.SetContentType(contentType)
1✔
146
        }
1✔
147

148
        return blob, nil
1✔
149
}
150

151
func (u *UploadLoader) validateContentType(contentType string) bool {
1✔
152
        if len(u.accepts) == 0 {
1✔
NEW
153
                return true // Accept all if no restrictions
×
NEW
154
        }
×
155

156
        if contentType == "" {
2✔
157
                return false
1✔
158
        }
1✔
159

160
        // Parse media type to ignore parameters
161
        mediaType, _, err := mime.ParseMediaType(contentType)
1✔
162
        if err != nil {
1✔
NEW
163
                return false
×
NEW
164
        }
×
165

166
        for _, accept := range u.accepts {
2✔
167
                if accept == "*/*" || accept == mediaType {
2✔
168
                        return true
1✔
169
                }
1✔
170
                // Handle wildcard types like "image/*"
171
                if strings.HasSuffix(accept, "/*") {
2✔
172
                        prefix := strings.TrimSuffix(accept, "/*")
1✔
173
                        if strings.HasPrefix(mediaType, prefix+"/") {
2✔
174
                                return true
1✔
175
                        }
1✔
176
                }
177
        }
178

179
        return false
1✔
180
}
181

182
func parseContentType(s string) string {
1✔
183
        s = strings.TrimSpace(s)
1✔
184
        if s == "" {
2✔
185
                return ""
1✔
186
        }
1✔
187
        mediaType, _, err := mime.ParseMediaType(s)
1✔
188
        if err != nil {
1✔
NEW
189
                return s // Return original string if parsing fails
×
NEW
190
        }
×
191
        return mediaType
1✔
192
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc