├── .travis.yml ├── errors.go ├── LICENSE ├── README.md ├── imup.go └── imup_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7.4 4 | - tip 5 | 6 | script: 7 | - go test -v ./... -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package imup 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrDisallowedType is returned when the uploaded 7 | // file type is not allowed. 8 | ErrDisallowedType = errors.New("File type is not allowed") 9 | 10 | // ErrFileSize is returned when the uploaded file 11 | // size exceeds the max file size limit. 12 | ErrFileSize = errors.New("File size exceeds max limit") 13 | ) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Conner Hewitt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imup [![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/beeker1121/imup) [![License](http://img.shields.io/badge/license-mit-blue.svg)](https://raw.githubusercontent.com/beeker1121/imup/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/beeker1121/imup)](https://goreportcard.com/report/github.com/beeker1121/imup) [![Build Status](https://travis-ci.org/beeker1121/imup.svg?branch=master)](https://travis-ci.org/beeker1121/imup) 2 | 3 | imup is an image upload handler written in Go. 4 | 5 | Managing image uploads over HTTP in Go can be difficult when taking into account file type and request length checks, handling request cancellation, and so on. This package was built to abstract those details away and provide a simple API to make dealing with image uploads easier. 6 | 7 | **Special Thanks** to [@vcabbage](https://github.com/vcabbage) for the brilliant max file size solution! 8 | 9 | This project was created for [MailDB.io](https://maildb.io/), check us out! 10 | 11 | ## Features 12 | 13 | - Handles all MIME image types defined in the standard `http` lib: 14 | GIF, PNG, JPEG, BMP, WEBP, ICO 15 | - Set allowable image formats 16 | - Supports max file size limit 17 | - Reads data only up to max file size limit, will not eat up bandwidth 18 | - Handles spoofed Content-Length header 19 | 20 | ## Installation 21 | 22 | Fetch the package from GitHub: 23 | 24 | ```sh 25 | go get github.com/beeker1121/imup 26 | ``` 27 | 28 | Import to your project: 29 | 30 | ```go 31 | import "github.com/beeker1121/imup" 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```go 37 | func handler(w http.ResponseWriter, r *http.Request) { 38 | // Parse the uploaded file. 39 | ui, err = imup.New("file", r, &imup.Options{ 40 | MaxFileSize: 1 * 1024 * 1024, // 1 MB 41 | AllowedTypes: imup.PopularTypes, 42 | }) 43 | if err != nil { 44 | ... 45 | } 46 | 47 | // Save the image. 48 | filename, err := ui.Save("images/test") 49 | if err != nil { 50 | ... 51 | } 52 | 53 | fmt.Println(filename) // images/test.png 54 | } 55 | ``` 56 | 57 | ## License 58 | 59 | MIT license -------------------------------------------------------------------------------- /imup.go: -------------------------------------------------------------------------------- 1 | package imup 2 | 3 | import ( 4 | "io" 5 | "mime/multipart" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | // ImageTypes defines the allowed types for an uploaded image. 12 | type ImageTypes []string 13 | 14 | // Image types according to the MIME specification. 15 | const ( 16 | GIF = "image/gif" 17 | PNG = "image/png" 18 | JPEG = "image/jpeg" 19 | BMP = "image/bmp" 20 | WEBP = "image/webp" 21 | ICO = "image/vnd.microsoft.icon" 22 | ) 23 | 24 | // Convenience set of image types. 25 | var ( 26 | PopularTypes = ImageTypes{GIF, PNG, JPEG} 27 | AllTypes = ImageTypes{GIF, PNG, JPEG, BMP, WEBP, ICO} 28 | ) 29 | 30 | // UploadedImage defines an uploaded image. 31 | type UploadedImage struct { 32 | Type string 33 | file multipart.File 34 | header *multipart.FileHeader 35 | } 36 | 37 | // Options defines the available options for an image upload. 38 | type Options struct { 39 | MaxFileSize int64 40 | AllowedTypes ImageTypes 41 | } 42 | 43 | // New returns a new UploadedImage object if the uploaded file could be parsed 44 | // and validated as an image, otherwise it returns an error. 45 | // 46 | // The key parameter should refer to the name of the file input from the 47 | // multipart form. 48 | func New(key string, r *http.Request, opts *Options) (*UploadedImage, error) { 49 | var err error 50 | ui := &UploadedImage{} 51 | 52 | // Handle max file size. 53 | if opts.MaxFileSize > 0 { 54 | // Check Content-Length header. 55 | cl, _ := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) 56 | if cl > opts.MaxFileSize { 57 | return nil, ErrFileSize 58 | } 59 | 60 | // Wrap r.Body with our limitReader. 61 | r.Body = newLimitReader(r.Body, opts.MaxFileSize) 62 | } 63 | 64 | // Try to parse the multipart file from the request. 65 | if ui.file, ui.header, err = r.FormFile(key); err != nil { 66 | return nil, err 67 | } 68 | 69 | // Check if type is allowed. 70 | if len(opts.AllowedTypes) > 0 { 71 | if err = isTypeAllowed(ui, opts.AllowedTypes); err != nil { 72 | return nil, err 73 | } 74 | } 75 | 76 | return ui, nil 77 | } 78 | 79 | // Save saves the uploaded image to the given location and returns the location 80 | // with the correct image extension added on. 81 | // 82 | // The underlying multipart image file is automatically closed. 83 | func (ui *UploadedImage) Save(filename string) (string, error) { 84 | // Handle the file extension. 85 | var ext string 86 | switch ui.Type { 87 | case GIF: 88 | ext = ".gif" 89 | case PNG: 90 | ext = ".png" 91 | case JPEG: 92 | ext = ".jpg" 93 | case BMP: 94 | ext = ".bmp" 95 | case WEBP: 96 | ext = ".webp" 97 | case ICO: 98 | ext = ".ico" 99 | } 100 | filename += ext 101 | 102 | // Create output file. 103 | out, err := os.Create(filename) 104 | if err != nil { 105 | return "", err 106 | } 107 | defer out.Close() 108 | 109 | // Store the uploaded image to output file. 110 | _, err = io.Copy(out, ui.file) 111 | if err != nil { 112 | return "", err 113 | } 114 | if err = ui.Close(); err != nil { 115 | return "", err 116 | } 117 | 118 | return filename, nil 119 | } 120 | 121 | // Close closes an uploaded image. 122 | func (ui *UploadedImage) Close() error { 123 | return ui.file.Close() 124 | } 125 | 126 | // isTypeAllowed checks if the given file type is allowed. 127 | func isTypeAllowed(ui *UploadedImage, types ImageTypes) error { 128 | // Get up to the first 512 bytes of data. 129 | b := make([]byte, 512) 130 | _, err := ui.file.Read(b) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | // Reset file pointer. 136 | if _, err = ui.file.Seek(0, 0); err != nil { 137 | return err 138 | } 139 | 140 | // Try to detect the file type. 141 | ui.Type = http.DetectContentType(b) 142 | 143 | // Validate type. 144 | for _, t := range types { 145 | if ui.Type == t { 146 | return nil 147 | } 148 | } 149 | 150 | return ErrDisallowedType 151 | } 152 | 153 | // limitReader defines our custom request body ReaderCloser type, which wraps 154 | // the standard io.LimitedReader. 155 | type limitReader struct { 156 | r *io.LimitedReader 157 | io.Closer 158 | } 159 | 160 | // newLimitReader creates a new limitReader. 161 | func newLimitReader(r io.ReadCloser, maxSize int64) io.ReadCloser { 162 | return &limitReader{ 163 | r: &io.LimitedReader{r, maxSize + 1}, 164 | Closer: r, 165 | } 166 | } 167 | 168 | // Read satisfies the io.Reader interface. 169 | // 170 | // ErrFileSize is returned when the limit is exceeded rather than io.EOF like 171 | // the standard io.LimitedReader. 172 | func (l *limitReader) Read(p []byte) (int, error) { 173 | n, err := l.r.Read(p) 174 | if l.r.N < 1 { 175 | return n, ErrFileSize 176 | } 177 | return n, err 178 | } 179 | -------------------------------------------------------------------------------- /imup_test.go: -------------------------------------------------------------------------------- 1 | package imup 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | "image/png" 8 | "io" 9 | "mime/multipart" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "testing" 14 | ) 15 | 16 | const ( 17 | testPNGName = "testpng.png" 18 | testTXTName = "testtxt.txt" 19 | ) 20 | 21 | // createTestPNG creates the test PNG image. 22 | func createTestPNG() error { 23 | // Create a new image. 24 | img := image.NewRGBA(image.Rect(0, 0, 100, 100)) 25 | 26 | // Draw a square. 27 | for x := 10; x < 90; x++ { 28 | for y := 10; y < 90; y++ { 29 | img.Set(x, y, color.RGBA{255, 0, 0, 255}) 30 | } 31 | } 32 | 33 | // Create the test PNG file. 34 | file, err := os.Create(testPNGName) 35 | if err != nil { 36 | return err 37 | } 38 | defer file.Close() 39 | 40 | // Save the image. 41 | png.Encode(file, img) 42 | 43 | return nil 44 | } 45 | 46 | // deleteTestPNG deletes the test PNG image. 47 | func deleteTestPNG() { 48 | os.Remove(testPNGName) 49 | } 50 | 51 | // createTestTXT create the test TXT file. 52 | func createTestTXT() error { 53 | // Create the test TXT file. 54 | file, err := os.Create(testTXTName) 55 | if err != nil { 56 | return err 57 | } 58 | defer file.Close() 59 | 60 | // Write data to the file. 61 | file.Write([]byte(`Lorem ipsum dolor sit amet`)) 62 | 63 | return nil 64 | } 65 | 66 | // deleteTestTXT deletes the test TXT file. 67 | func deleteTestTXT() { 68 | os.Remove(testTXTName) 69 | } 70 | 71 | // createRequest creates a test request. 72 | func createRequest(filename string) (*http.Request, error) { 73 | // Get file handler. 74 | file, err := os.Open(filename) 75 | if err != nil { 76 | return nil, err 77 | } 78 | defer file.Close() 79 | 80 | // Create a multipart writer. 81 | var b bytes.Buffer 82 | mw := multipart.NewWriter(&b) 83 | 84 | // Create a new form file. 85 | fw, err := mw.CreateFormFile("file", filename) 86 | if err != nil { 87 | return nil, err 88 | } 89 | if _, err = io.Copy(fw, file); err != nil { 90 | return nil, err 91 | } 92 | mw.Close() 93 | 94 | // Create a new test request. 95 | req := httptest.NewRequest("POST", "http://127.0.0.1/", &b) 96 | 97 | // Set the multipart/form-data Content-Type. 98 | req.Header.Set("Content-Type", mw.FormDataContentType()) 99 | 100 | return req, nil 101 | } 102 | 103 | func TestNewAndSave(t *testing.T) { 104 | err := createTestPNG() 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | defer deleteTestPNG() 109 | 110 | // Create a new test request. 111 | req, err := createRequest(testPNGName) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | // Upload the image. 117 | ui, err := New("file", req, &Options{ 118 | MaxFileSize: 1024, 119 | AllowedTypes: PopularTypes, 120 | }) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | // Save the image. 126 | filename, err := ui.Save("testsave") 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | if err = os.Remove(filename); err != nil { 132 | t.Fatal(err) 133 | } 134 | } 135 | 136 | func TestContentLength(t *testing.T) { 137 | err := createTestPNG() 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | defer deleteTestPNG() 142 | 143 | // Create a new test request. 144 | req, err := createRequest(testPNGName) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | 149 | req.Header.Set("Content-Length", "1024") 150 | 151 | // Upload the image. 152 | _, err = New("file", req, &Options{ 153 | MaxFileSize: 1024, 154 | AllowedTypes: PopularTypes, 155 | }) 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | } 160 | 161 | func TestErrFileSize(t *testing.T) { 162 | err := createTestPNG() 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | defer deleteTestPNG() 167 | 168 | // Create a new test request. 169 | req, err := createRequest(testPNGName) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | req.Header.Set("Content-Length", "1025") 175 | 176 | // Upload the image. 177 | _, err = New("file", req, &Options{ 178 | MaxFileSize: 1024, 179 | AllowedTypes: PopularTypes, 180 | }) 181 | if err != ErrFileSize { 182 | t.Errorf("Expected ErrFileSize, got %s", err) 183 | } 184 | } 185 | 186 | func TestErrFileSizeSpoofed(t *testing.T) { 187 | err := createTestPNG() 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | defer deleteTestPNG() 192 | 193 | // Create a new test request. 194 | req, err := createRequest(testPNGName) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | req.Header.Set("Content-Length", "128") 200 | 201 | // Upload the image. 202 | _, err = New("file", req, &Options{ 203 | MaxFileSize: 128, 204 | AllowedTypes: PopularTypes, 205 | }) 206 | if err != ErrFileSize { 207 | t.Errorf("Expected ErrFileSize, got %s", err) 208 | } 209 | } 210 | 211 | func TestErrDisallowedType(t *testing.T) { 212 | err := createTestTXT() 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | defer deleteTestTXT() 217 | 218 | // Create a new test request. 219 | req, err := createRequest(testTXTName) 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | 224 | // Upload the image. 225 | _, err = New("file", req, &Options{ 226 | MaxFileSize: 1024, 227 | AllowedTypes: PopularTypes, 228 | }) 229 | if err != ErrDisallowedType { 230 | t.Errorf("Expected ErrDisallowedType, got %s", err) 231 | } 232 | } 233 | --------------------------------------------------------------------------------