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

cshum / imagor / 18087185089

29 Sep 2025 05:50AM UTC coverage: 91.95% (-0.6%) from 92.565%
18087185089

Pull #617

github

cshum
improve test cases
Pull Request #617: feat: POST Upload Endpoint and Upload Form

215 of 270 new or added lines in 6 files covered. (79.63%)

55 existing lines in 1 file now uncovered.

5266 of 5727 relevant lines covered (91.95%)

1.1 hits per line

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

83.93
/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, we ignore the key parameter since there's no source URL
58
        // The key is typically empty or a special identifier for POST uploads
59

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

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

70
        // Handle raw body upload
71
        return u.handleRawUpload(r)
1✔
72
}
73

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

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

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

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

100
        return u.createBlobFromReader(file, header.Size, header.Header.Get("Content-Type"))
1✔
101
}
102

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

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

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

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

1✔
120
        return u.createBlobFromReader(limitedReader, contentLength, contentType)
1✔
121
}
122

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

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

137
                actualSize := int64(len(data))
1✔
138
                return io.NopCloser(strings.NewReader(string(data))), actualSize, nil
1✔
139
        })
140

141
        if contentType != "" {
2✔
142
                blob.SetContentType(contentType)
1✔
143
        }
1✔
144

145
        return blob, nil
1✔
146
}
147

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

153
        if contentType == "" {
2✔
154
                return false
1✔
155
        }
1✔
156

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

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

176
        return false
1✔
177
}
178

179
func parseContentType(s string) string {
1✔
180
        s = strings.TrimSpace(s)
1✔
181
        if s == "" {
2✔
182
                return ""
1✔
183
        }
1✔
184
        mediaType, _, err := mime.ParseMediaType(s)
1✔
185
        if err != nil {
1✔
NEW
186
                return s // Return original string if parsing fails
×
NEW
187
        }
×
188
        return mediaType
1✔
189
}
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