├── go.mod ├── .travis.yml ├── .gitignore ├── LICENSE ├── fuh.go ├── go.sum ├── file_store_test.go ├── file_store.go ├── README.md ├── context.go ├── handler.go └── fuh_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/LyricTian/fuh 2 | 3 | go 1.13 4 | 5 | require github.com/smartystreets/goconvey v1.6.4 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go_import_path: github.com/LyricTian/fuh 4 | go: 5 | - 1.7 6 | before_install: 7 | - go get -t -v ./... 8 | 9 | script: 10 | - go test -race -coverprofile=coverage.txt -covermode=atomic 11 | 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | testdatas/upload 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | /testdatas -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lyric 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. 22 | -------------------------------------------------------------------------------- /fuh.go: -------------------------------------------------------------------------------- 1 | package fuh 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "sync" 8 | ) 9 | 10 | // FileInfo upload the basic information of the file 11 | type FileInfo interface { 12 | FullName() string 13 | Name() string 14 | Size() int64 15 | } 16 | 17 | // Uploader file upload interface 18 | type Uploader interface { 19 | Upload(ctx context.Context, r *http.Request, key string) ([]FileInfo, error) 20 | } 21 | 22 | // Storer file storage interface 23 | type Storer interface { 24 | Store(ctx context.Context, filename string, data io.Reader, size int64) error 25 | } 26 | 27 | var ( 28 | internalHandle *uploadHandle 29 | once sync.Once 30 | ) 31 | 32 | func uploader() *uploadHandle { 33 | once.Do(func() { 34 | internalHandle = &uploadHandle{} 35 | }) 36 | return internalHandle 37 | } 38 | 39 | // SetConfig set the configuration parameters 40 | func SetConfig(cfg *Config) { 41 | uploader().cfg = cfg 42 | } 43 | 44 | // SetStore set storage 45 | func SetStore(store Storer) { 46 | uploader().store = store 47 | } 48 | 49 | // Upload file upload 50 | func Upload(ctx context.Context, r *http.Request, key string) ([]FileInfo, error) { 51 | return uploader().Upload(ctx, r, key) 52 | } 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 2 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 3 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 4 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 5 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 6 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 7 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 8 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 13 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 14 | -------------------------------------------------------------------------------- /file_store_test.go: -------------------------------------------------------------------------------- 1 | package fuh_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/LyricTian/fuh" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestFileStore(t *testing.T) { 15 | basePath := "testdatas/" 16 | Convey("file storage test", t, func() { 17 | 18 | Convey("write data", func() { 19 | store := fuh.NewFileStore() 20 | buf := []byte("abc") 21 | filename := filepath.Join(basePath, "write.txt") 22 | 23 | err := store.Store(nil, filename, bytes.NewReader(buf), int64(len(buf))) 24 | So(err, ShouldBeNil) 25 | 26 | if err == nil { 27 | defer os.Remove(filename) 28 | } 29 | 30 | file, err := os.Open(filename) 31 | So(err, ShouldBeNil) 32 | defer file.Close() 33 | 34 | fbuf, err := ioutil.ReadAll(file) 35 | So(err, ShouldBeNil) 36 | So(string(fbuf), ShouldEqual, string(buf)) 37 | }) 38 | 39 | Convey("rewrite data", func() { 40 | filename := filepath.Join(basePath, "rewrite.txt") 41 | 42 | cfile, err := os.Create(filename) 43 | So(err, ShouldBeNil) 44 | 45 | defer os.Remove(filename) 46 | 47 | cfile.Write([]byte("123")) 48 | cfile.Close() 49 | 50 | buf := []byte("abc") 51 | store := &fuh.FileStore{Rewrite: true} 52 | err = store.Store(nil, filename, bytes.NewReader(buf), int64(len(buf))) 53 | So(err, ShouldBeNil) 54 | 55 | ofile, err := os.Open(filename) 56 | So(err, ShouldBeNil) 57 | defer ofile.Close() 58 | 59 | fbuf, err := ioutil.ReadAll(ofile) 60 | So(err, ShouldBeNil) 61 | So(string(fbuf), ShouldEqual, string(buf)) 62 | }) 63 | 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /file_store.go: -------------------------------------------------------------------------------- 1 | package fuh 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var ( 12 | // ErrNoData no data is stored 13 | ErrNoData = errors.New("no data is stored") 14 | // ErrFileExists file already exists 15 | ErrFileExists = errors.New("file already exists") 16 | ) 17 | 18 | // NewFileStore create a file store 19 | func NewFileStore() *FileStore { 20 | return &FileStore{} 21 | } 22 | 23 | // NewFileStoreWithBasePath create a file store with base path 24 | func NewFileStoreWithBasePath(basePath string) *FileStore { 25 | return &FileStore{ 26 | BasePath: basePath, 27 | } 28 | } 29 | 30 | // FileStore file storage 31 | type FileStore struct { 32 | // rewrite the existing file 33 | Rewrite bool 34 | BasePath string 35 | } 36 | 37 | func (f *FileStore) exists(filename string) (exists bool, err error) { 38 | exists = true 39 | _, verr := os.Stat(filename) 40 | if verr != nil { 41 | if os.IsNotExist(verr) { 42 | exists = false 43 | return 44 | } 45 | err = verr 46 | } 47 | return 48 | } 49 | 50 | // Store store data to a local file 51 | func (f *FileStore) Store(ctx context.Context, filename string, data io.Reader, size int64) error { 52 | if filename == "" || data == nil || size == 0 { 53 | return ErrNoData 54 | } 55 | 56 | if f.BasePath != "" { 57 | filename = filepath.Join(f.BasePath, filename) 58 | } 59 | 60 | if ctx == nil { 61 | ctx = context.Background() 62 | } 63 | 64 | exists, err := f.exists(filename) 65 | if err != nil { 66 | return err 67 | } else if exists { 68 | if !f.Rewrite { 69 | return ErrFileExists 70 | } 71 | os.Remove(filename) 72 | } 73 | 74 | dir := filepath.Dir(filename) 75 | if dir != "" { 76 | if exists, err := f.exists(dir); err != nil { 77 | return err 78 | } else if !exists { 79 | err = os.MkdirAll(dir, os.ModePerm) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | } 85 | 86 | file, err := os.Create(filename) 87 | if err != nil { 88 | return err 89 | } 90 | defer file.Close() 91 | 92 | _, err = io.CopyN(file, data, size) 93 | return err 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang File Upload Handler 2 | 3 | [![Build][Build-Status-Image]][Build-Status-Url] [![Codecov][codecov-image]][codecov-url] [![ReportCard][reportcard-image]][reportcard-url] [![GoDoc][godoc-image]][godoc-url] [![License][license-image]][license-url] 4 | 5 | ## Quick Start 6 | 7 | ### Download and install 8 | 9 | ```bash 10 | go get -v github.com/LyricTian/fuh 11 | ``` 12 | 13 | ### Create file `server.go` 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "net/http" 22 | "path/filepath" 23 | 24 | "github.com/LyricTian/fuh" 25 | ) 26 | 27 | func main() { 28 | upl := fuh.NewUploader(&fuh.Config{ 29 | BasePath: "attach", 30 | SizeLimit: 1 << 20, 31 | }, fuh.NewFileStore()) 32 | 33 | http.HandleFunc("/fileupload", func(w http.ResponseWriter, r *http.Request) { 34 | 35 | ctx := fuh.NewFileNameContext(context.Background(), func(ci fuh.ContextInfo) string { 36 | return filepath.Join(ci.BasePath(), ci.FileName()) 37 | }) 38 | 39 | finfos, err := upl.Upload(ctx, r, "file") 40 | if err != nil { 41 | w.WriteHeader(500) 42 | return 43 | } 44 | json.NewEncoder(w).Encode(finfos) 45 | }) 46 | 47 | http.ListenAndServe(":8080", nil) 48 | } 49 | ``` 50 | 51 | ### Build and run 52 | 53 | ```bash 54 | $ go build server.go 55 | $ ./server 56 | ``` 57 | 58 | ## Features 59 | 60 | - Custom file name 61 | - Custom file size limit 62 | - Supports storage extensions 63 | - Context support 64 | 65 | ## MIT License 66 | 67 | Copyright (c) 2017 Lyric 68 | 69 | [Build-Status-Url]: https://travis-ci.org/LyricTian/fuh 70 | [Build-Status-Image]: https://travis-ci.org/LyricTian/fuh.svg?branch=master 71 | [codecov-url]: https://codecov.io/gh/LyricTian/fuh 72 | [codecov-image]: https://codecov.io/gh/LyricTian/fuh/branch/master/graph/badge.svg 73 | [reportcard-url]: https://goreportcard.com/report/github.com/LyricTian/fuh 74 | [reportcard-image]: https://goreportcard.com/badge/github.com/LyricTian/fuh 75 | [godoc-url]: https://godoc.org/github.com/LyricTian/fuh 76 | [godoc-image]: https://godoc.org/github.com/LyricTian/fuh?status.svg 77 | [license-url]: http://opensource.org/licenses/MIT 78 | [license-image]: https://img.shields.io/npm/l/express.svg -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package fuh 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/textproto" 7 | ) 8 | 9 | type ( 10 | fileSizeLimitKey struct{} 11 | fileNameKey struct{} 12 | contextInfoKey struct{} 13 | 14 | // ContextInfo the context information 15 | ContextInfo interface { 16 | BasePath() string 17 | FileName() string 18 | FileSize() int64 19 | FileHeader() textproto.MIMEHeader 20 | Request() *http.Request 21 | } 22 | 23 | // FileSizeLimitHandle file size limit 24 | FileSizeLimitHandle func(ci ContextInfo) bool 25 | 26 | // FileNameHandle the file name 27 | FileNameHandle func(ci ContextInfo) string 28 | ) 29 | 30 | // NewFileSizeLimitContext returns a new Context that carries value fsl. 31 | func NewFileSizeLimitContext(ctx context.Context, fsl FileSizeLimitHandle) context.Context { 32 | return context.WithValue(ctx, fileSizeLimitKey{}, fsl) 33 | } 34 | 35 | // FromFileSizeLimitContext returns the FileSizeLimitHandle value stored in ctx, if any. 36 | func FromFileSizeLimitContext(ctx context.Context) (FileSizeLimitHandle, bool) { 37 | handle, ok := ctx.Value(fileSizeLimitKey{}).(FileSizeLimitHandle) 38 | return handle, ok 39 | } 40 | 41 | // NewFileNameContext returns a new Context that carries value fn. 42 | func NewFileNameContext(ctx context.Context, fn FileNameHandle) context.Context { 43 | return context.WithValue(ctx, fileNameKey{}, fn) 44 | } 45 | 46 | // FromFileNameContext returns the FileNameHandle value stored in ctx, if any. 47 | func FromFileNameContext(ctx context.Context) (FileNameHandle, bool) { 48 | handle, ok := ctx.Value(fileNameKey{}).(FileNameHandle) 49 | return handle, ok 50 | } 51 | 52 | // NewContextInfoContext returns a new Context that context information ci. 53 | func NewContextInfoContext(ctx context.Context, ci ContextInfo) context.Context { 54 | return context.WithValue(ctx, contextInfoKey{}, ci) 55 | } 56 | 57 | // FromContextInfoContext returns the ContextInfo value stored in ctx, if any. 58 | func FromContextInfoContext(ctx context.Context) (ContextInfo, bool) { 59 | info, ok := ctx.Value(contextInfoKey{}).(ContextInfo) 60 | return info, ok 61 | } 62 | 63 | type contextInfo struct { 64 | basePath string 65 | fileName string 66 | fileSize int64 67 | fileHeader textproto.MIMEHeader 68 | req *http.Request 69 | } 70 | 71 | func (ci *contextInfo) BasePath() string { 72 | return ci.basePath 73 | } 74 | 75 | func (ci *contextInfo) FileName() string { 76 | return ci.fileName 77 | } 78 | 79 | func (ci *contextInfo) FileSize() int64 { 80 | return ci.fileSize 81 | } 82 | 83 | func (ci *contextInfo) FileHeader() textproto.MIMEHeader { 84 | return ci.fileHeader 85 | } 86 | 87 | func (ci *contextInfo) Request() *http.Request { 88 | return ci.req 89 | } 90 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package fuh 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "mime/multipart" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | const ( 13 | defaultMaxMemory = 32 << 20 // 32 MB 14 | ) 15 | 16 | var ( 17 | // ErrMissingFile no such file 18 | ErrMissingFile = errors.New("no such file") 19 | // ErrFileTooLarge file too large 20 | ErrFileTooLarge = errors.New("file too large") 21 | ) 22 | 23 | // Config basic configuration 24 | type Config struct { 25 | BasePath string 26 | SizeLimit int64 27 | MaxMemory int64 28 | } 29 | 30 | // NewUploader create a file upload interface 31 | func NewUploader(cfg *Config, store Storer) Uploader { 32 | return &uploadHandle{ 33 | cfg: cfg, 34 | store: store, 35 | } 36 | } 37 | 38 | type uploadHandle struct { 39 | cfg *Config 40 | store Storer 41 | } 42 | 43 | func (u *uploadHandle) Upload(ctx context.Context, r *http.Request, key string) ([]FileInfo, error) { 44 | if ctx == nil { 45 | ctx = context.Background() 46 | } 47 | 48 | if u.store == nil { 49 | u.store = NewFileStore() 50 | } 51 | 52 | if r.MultipartForm == nil { 53 | err := r.ParseMultipartForm(u.maxMemory()) 54 | if err != nil { 55 | return nil, err 56 | } 57 | } 58 | 59 | if r.MultipartForm == nil || r.MultipartForm.File == nil { 60 | return nil, ErrMissingFile 61 | } 62 | 63 | var infos []FileInfo 64 | for _, file := range r.MultipartForm.File[key] { 65 | info, err := u.uploadDo(ctx, r, file) 66 | if err != nil { 67 | return infos, err 68 | } 69 | infos = append(infos, info) 70 | } 71 | return infos, nil 72 | } 73 | 74 | func (u *uploadHandle) config() *Config { 75 | if c := u.cfg; c != nil { 76 | return c 77 | } 78 | return &Config{} 79 | } 80 | 81 | func (u *uploadHandle) maxMemory() int64 { 82 | if mm := u.config().MaxMemory; mm > 0 { 83 | return mm 84 | } 85 | return defaultMaxMemory 86 | } 87 | 88 | func (u *uploadHandle) uploadDo(ctx context.Context, r *http.Request, fheader *multipart.FileHeader) (FileInfo, error) { 89 | file, err := fheader.Open() 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer file.Close() 94 | 95 | size, err := u.fileSize(file) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | ctxInfo := &contextInfo{ 101 | basePath: u.config().BasePath, 102 | fileName: fheader.Filename, 103 | fileSize: size, 104 | fileHeader: fheader.Header, 105 | req: r, 106 | } 107 | ctx = NewContextInfoContext(ctx, ctxInfo) 108 | 109 | if h, ok := FromFileSizeLimitContext(ctx); ok { 110 | if !h(ctxInfo) { 111 | return nil, ErrFileTooLarge 112 | } 113 | } else if sl := u.config().SizeLimit; sl > 0 && size > sl { 114 | return nil, ErrFileTooLarge 115 | } 116 | 117 | var fullName string 118 | if h, ok := FromFileNameContext(ctx); ok { 119 | fullName = h(ctxInfo) 120 | } else { 121 | fullName = filepath.Join(ctxInfo.BasePath(), ctxInfo.FileName()) 122 | } 123 | 124 | err = u.store.Store(ctx, fullName, file, size) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | return &fileInfo{ 130 | fullName: fullName, 131 | name: ctxInfo.FileName(), 132 | size: size, 133 | }, nil 134 | } 135 | 136 | // get the size of the uploaded file 137 | func (u *uploadHandle) fileSize(file multipart.File) (int64, error) { 138 | var size int64 139 | 140 | if fsize, ok := file.(fsize); ok { 141 | size = fsize.Size() 142 | } else if fstat, ok := file.(fstat); ok { 143 | stat, err := fstat.Stat() 144 | if err != nil { 145 | return 0, err 146 | } 147 | size = stat.Size() 148 | } 149 | 150 | return size, nil 151 | } 152 | 153 | type fsize interface { 154 | Size() int64 155 | } 156 | 157 | type fstat interface { 158 | Stat() (os.FileInfo, error) 159 | } 160 | 161 | type fileInfo struct { 162 | fullName string 163 | name string 164 | size int64 165 | } 166 | 167 | func (fi *fileInfo) FullName() string { 168 | return fi.fullName 169 | } 170 | 171 | func (fi *fileInfo) Name() string { 172 | return fi.name 173 | } 174 | 175 | func (fi *fileInfo) Size() int64 { 176 | return fi.size 177 | } 178 | -------------------------------------------------------------------------------- /fuh_test.go: -------------------------------------------------------------------------------- 1 | package fuh_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "log" 8 | "mime/multipart" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/LyricTian/fuh" 16 | . "github.com/smartystreets/goconvey/convey" 17 | ) 18 | 19 | func TestFileUpload(t *testing.T) { 20 | basePath := "testdatas/" 21 | filename := "single_test.txt" 22 | buf := []byte("abc") 23 | 24 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | Convey("single file upload test", t, func() { 26 | config := &fuh.Config{BasePath: basePath, SizeLimit: 1 << 20, MaxMemory: 10 << 20} 27 | 28 | fuh.SetConfig(config) 29 | fuh.SetStore(fuh.NewFileStore()) 30 | 31 | fileInfos, err := fuh.Upload(nil, r, "file") 32 | So(err, ShouldBeNil) 33 | So(len(fileInfos), ShouldEqual, 1) 34 | So(fileInfos[0].Size(), ShouldEqual, len(buf)) 35 | So(fileInfos[0].FullName(), ShouldEqual, filepath.Join(basePath, filename)) 36 | 37 | defer os.Remove(fileInfos[0].FullName()) 38 | 39 | file, err := os.Open(fileInfos[0].FullName()) 40 | So(err, ShouldBeNil) 41 | defer file.Close() 42 | 43 | buf, err := ioutil.ReadAll(file) 44 | So(err, ShouldBeNil) 45 | So(string(buf), ShouldEqual, string(buf)) 46 | }) 47 | })) 48 | defer srv.Close() 49 | 50 | postFile(srv.URL, buf, filename) 51 | } 52 | 53 | func TestCustomFileName(t *testing.T) { 54 | basePath := "testdatas/" 55 | filename := "filename_test.txt" 56 | buf := []byte("abc") 57 | 58 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 59 | Convey("custom file name test", t, func() { 60 | uploader := fuh.NewUploader(&fuh.Config{BasePath: basePath}, fuh.NewFileStore()) 61 | 62 | nfilename := "nfilename_test.txt" 63 | ctx := fuh.NewFileNameContext(context.Background(), func(ci fuh.ContextInfo) string { 64 | return filepath.Join(ci.BasePath(), nfilename) 65 | }) 66 | 67 | fileInfos, err := uploader.Upload(ctx, r, "file") 68 | So(err, ShouldBeNil) 69 | So(len(fileInfos), ShouldEqual, 1) 70 | So(fileInfos[0].Size(), ShouldEqual, len(buf)) 71 | So(fileInfos[0].FullName(), ShouldEqual, filepath.Join(basePath, nfilename)) 72 | 73 | defer os.Remove(fileInfos[0].FullName()) 74 | 75 | file, err := os.Open(fileInfos[0].FullName()) 76 | So(err, ShouldBeNil) 77 | defer file.Close() 78 | 79 | buf, err := ioutil.ReadAll(file) 80 | So(err, ShouldBeNil) 81 | So(string(buf), ShouldEqual, string(buf)) 82 | }) 83 | })) 84 | defer srv.Close() 85 | 86 | postFile(srv.URL, buf, filename) 87 | } 88 | 89 | func TestFileSizeLimit(t *testing.T) { 90 | basePath := "testdatas/" 91 | filename := "filesizelimit_test.txt" 92 | buf := []byte("abc") 93 | 94 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 95 | Convey("file size limit test", t, func() { 96 | uploader := fuh.NewUploader(&fuh.Config{BasePath: basePath}, fuh.NewFileStore()) 97 | 98 | ctx := fuh.NewFileSizeLimitContext(context.Background(), func(ci fuh.ContextInfo) bool { 99 | return ci.FileSize() < 3 100 | }) 101 | 102 | fileInfos, err := uploader.Upload(ctx, r, "file") 103 | So(fileInfos, ShouldBeNil) 104 | So(err, ShouldNotBeNil) 105 | So(err, ShouldEqual, fuh.ErrFileTooLarge) 106 | }) 107 | })) 108 | defer srv.Close() 109 | 110 | postFile(srv.URL, buf, filename) 111 | } 112 | 113 | func TestFileSize(t *testing.T) { 114 | basePath := "testdatas/" 115 | filename := "filesize_test.txt" 116 | buf := make([]byte, 1024) 117 | for i := 0; i < 1024; i++ { 118 | buf[i] = '0' 119 | } 120 | 121 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 122 | Convey("file size test", t, func() { 123 | uploader := fuh.NewUploader(&fuh.Config{BasePath: basePath, MaxMemory: 128}, fuh.NewFileStore()) 124 | 125 | defer os.Remove(filepath.Join(basePath, filename)) 126 | 127 | fileInfos, err := uploader.Upload(context.Background(), r, "file") 128 | So(err, ShouldBeNil) 129 | So(len(fileInfos), ShouldEqual, 1) 130 | So(fileInfos[0].Size(), ShouldEqual, len(buf)) 131 | 132 | }) 133 | })) 134 | defer srv.Close() 135 | 136 | postFile(srv.URL, buf, filename) 137 | } 138 | 139 | func postFile(targetURL string, data []byte, filenames ...string) { 140 | bodyBuf := &bytes.Buffer{} 141 | bodyWriter := multipart.NewWriter(bodyBuf) 142 | 143 | for _, filename := range filenames { 144 | fileWriter, err := bodyWriter.CreateFormFile("file", filename) 145 | if err != nil { 146 | log.Println(err.Error()) 147 | return 148 | } 149 | fileWriter.Write(data) 150 | } 151 | 152 | contentType := bodyWriter.FormDataContentType() 153 | bodyWriter.Close() 154 | 155 | http.Post(targetURL, contentType, bodyBuf) 156 | 157 | return 158 | } 159 | --------------------------------------------------------------------------------