diff --git a/HISTORY.md b/HISTORY.md index 2208b54a..aaacb6e0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -599,6 +599,7 @@ New Package-level Variables: New Context Methods: +- `Context.SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error)` previously unexported. Accepts a result file of `Context.FormFile` and saves it to the disk. - `Context.URLParamSlice(name string) []string` is a a shortcut of `ctx.Request().URL.Query()[name]`. Like `URLParam` but it returns all values as a string slice instead of a single string separated by commas. - `Context.PostValueMany(name string) (string, error)` returns the post data of a given key. The returned value is a single string separated by commas on multiple values. It also reports whether the form was empty or when the "name" does not exist or whether the available values are empty. It strips any empty key-values from the slice before return. See `ErrEmptyForm`, `ErrNotFound` and `ErrEmptyFormField` respectfully. The `PostValueInt`, `PostValueInt64`, `PostValueFloat64` and `PostValueBool` now respect the above errors too (the `PostValues` method now returns a second output argument of `error` too, see breaking changes below). - `Context.URLParamsSorted() []memstore.StringEntry` returns a sorted (by key) slice of key-value entries of the URL Query parameters. @@ -637,6 +638,7 @@ New Context Methods: Breaking Changes: +- `ContextUploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error)` now returns the total files uploaded too (as its first parameter) and the "before" variadic option should return a boolean, if false then the specific file is skipped. - `Context.PostValues(name string) ([]string, error)` now returns a second output argument of `error` type too, which reports `ErrEmptyForm` or `ErrNotFound` or `ErrEmptyFormField`. The single post value getters now returns the **last value** if multiple was given instead of the first one (this allows clients to append values on flow updates). - `Party.GetReporter()` **removed**. The `Application.Build` returns the first error now and the API's errors are logged, this allows the server to run even if some of the routes are invalid but not fatal to the entire application (it was a request from a company). - `versioning.NewGroup(string)` now accepts a `Party` as its first input argument: `NewGroup(Party, string)`. diff --git a/_examples/file-server/file-server/main.go b/_examples/file-server/file-server/main.go index 3032a976..f353adc2 100644 --- a/_examples/file-server/file-server/main.go +++ b/_examples/file-server/file-server/main.go @@ -102,7 +102,7 @@ func uploadView(ctx iris.Context) { func upload(ctx iris.Context) { ctx.SetMaxRequestBodySize(maxSize) - _, err := ctx.UploadFormFiles(uploadDir, beforeSave) + _, _, err := ctx.UploadFormFiles(uploadDir, beforeSave) if err != nil { ctx.StopWithError(iris.StatusPayloadTooRage, err) return @@ -111,12 +111,13 @@ func upload(ctx iris.Context) { ctx.Redirect("/files") } -func beforeSave(ctx iris.Context, file *multipart.FileHeader) { +func beforeSave(ctx iris.Context, file *multipart.FileHeader) bool { ip := ctx.RemoteAddr() ip = strings.ReplaceAll(ip, ".", "_") ip = strings.ReplaceAll(ip, ":", "_") file.Filename = ip + "-" + file.Filename + return true } func deleteFile(ctx iris.Context) { diff --git a/_examples/file-server/upload-files/main.go b/_examples/file-server/upload-files/main.go index e1094daa..2f23f27a 100644 --- a/_examples/file-server/upload-files/main.go +++ b/_examples/file-server/upload-files/main.go @@ -5,8 +5,6 @@ import ( "fmt" "io" "mime/multipart" - "os" - "path/filepath" "strconv" "strings" "time" @@ -66,7 +64,7 @@ func newApp() *iris.Application { files := form.File["files[]"] failures := 0 for _, file := range files { - _, err = saveUploadedFile(file, "./uploads") + _, err = ctx.SaveFormFile(file, "./uploads/"+file.Filename) if err != nil { failures++ ctx.Writef("failed to upload: %s\n", file.Filename) @@ -78,24 +76,7 @@ func newApp() *iris.Application { return app } -func saveUploadedFile(fh *multipart.FileHeader, destDirectory string) (int64, error) { - src, err := fh.Open() - if err != nil { - return 0, err - } - defer src.Close() - - out, err := os.OpenFile(filepath.Join(destDirectory, fh.Filename), - os.O_WRONLY|os.O_CREATE, os.FileMode(0666)) - if err != nil { - return 0, err - } - defer out.Close() - - return io.Copy(out, src) -} - -func beforeSave(ctx iris.Context, file *multipart.FileHeader) { +func beforeSave(ctx iris.Context, file *multipart.FileHeader) bool { ip := ctx.RemoteAddr() // make sure you format the ip in a way // that can be used for a file name (simple case): @@ -109,8 +90,9 @@ func beforeSave(ctx iris.Context, file *multipart.FileHeader) { // no need for more actions, internal uploader will use this // name to save the file into the "./uploads" folder. if ip == "" { - return + return true // don't change the file but continue saving it. } file.Filename = ip + "-" + file.Filename + return true } diff --git a/_examples/request-body/read-body/main.go b/_examples/request-body/read-body/main.go index 001edca4..ebf2ced1 100644 --- a/_examples/request-body/read-body/main.go +++ b/_examples/request-body/read-body/main.go @@ -30,7 +30,7 @@ func readBody(ctx iris.Context) { var p payload // Bind request body to "p" depending on the content-type that client sends the data, - // e.g. JSON, XML, YAML, MessagePack, Form, URL Query. + // e.g. JSON, XML, YAML, MessagePack, Protobuf, Form and URL Query. err := ctx.ReadBody(&p) if err != nil { ctx.StopWithProblem(iris.StatusBadRequest, diff --git a/context/context.go b/context/context.go index 47b3aea6..d4abc0c3 100644 --- a/context/context.go +++ b/context/context.go @@ -1925,7 +1925,7 @@ func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, // to the system physical location "destDirectory". // // The second optional argument "before" gives caller the chance to -// modify the *miltipart.FileHeader before saving to the disk, +// modify or cancel the *miltipart.FileHeader before saving to the disk, // it can be used to change a file's name based on the current request, // all FileHeader's options can be changed. You can ignore it if // you don't need to use this capability before saving a file to the disk. @@ -1938,52 +1938,60 @@ func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, // http.ErrMissingFile if no file received. // // If you want to receive & accept files and manage them manually you can use the `context#FormFile` -// instead and create a copy function that suits your needs, the below is for generic usage. +// instead and create a copy function that suits your needs or use the `SaveFormFile` method, +// the below is for generic usage. // -// The default form's memory maximum size is 32MB, it can be changed by the -// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. +// The default form's memory maximum size is 32MB, it can be changed by +// the `WithPostMaxMemory` configurator or by `SetMaxRequestBodySize` or +// by the `LimitRequestBodySize` middleware (depends the use case). // -// See `FormFile` to a more controlled to receive a file. +// See `FormFile` to a more controlled way to receive a file. // // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/upload-files -func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader)) (n int64, err error) { +func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error) { err = ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory()) if err != nil { - return 0, err + return nil, 0, err } if ctx.request.MultipartForm != nil { if fhs := ctx.request.MultipartForm.File; fhs != nil { for _, files := range fhs { + innerLoop: for _, file := range files { for _, b := range before { - b(ctx, file) + if !b(ctx, file) { + continue innerLoop + } } - n0, err0 := uploadTo(file, destDirectory) + n0, err0 := ctx.SaveFormFile(file, filepath.Join(destDirectory, file.Filename)) if err0 != nil { - return 0, err0 + return nil, 0, err0 } n += n0 + + uploaded = append(uploaded, file) } } - return n, nil + return uploaded, n, nil } } - return 0, http.ErrMissingFile + return nil, 0, http.ErrMissingFile } -func uploadTo(fh *multipart.FileHeader, destDirectory string) (int64, error) { +// SaveFormFile saves a result of `FormFile` to the "dest" disk full path (directory + filename). +// See `FormFile` and `UploadFormFiles` too. +func (ctx *Context) SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error) { src, err := fh.Open() if err != nil { return 0, err } defer src.Close() - out, err := os.OpenFile(filepath.Join(destDirectory, fh.Filename), - os.O_WRONLY|os.O_CREATE, os.FileMode(0666)) + out, err := os.Create(dest) if err != nil { return 0, err } @@ -2204,6 +2212,35 @@ var ( // Usage: errors.Is(err, ErrEmptyFormField) // See postValue method. It's only returned on parsed post value methods. ErrEmptyFormField = errors.New("empty form field") + + // ConnectionCloseErrorSubstr if at least one of the given + // substrings are found in a net.OpError:os.SyscallError error type + // on `IsErrConnectionReset` then the function will report true. + ConnectionCloseErrorSubstr = []string{ + "broken pipe", + "connection reset by peer", + } + + // IsErrConnectionClosed reports whether the given "err" + // is caused because of a broken connection. + IsErrConnectionClosed = func(err error) bool { + if err == nil { + return false + } + + if opErr, ok := err.(*net.OpError); ok { + if syscallErr, ok := opErr.Err.(*os.SyscallError); ok { + errStr := strings.ToLower(syscallErr.Error()) + for _, s := range ConnectionCloseErrorSubstr { + if strings.Contains(errStr, s) { + return true + } + } + } + } + + return false + } ) // ReadForm binds the request body of a form to the "formObject". diff --git a/middleware/recover/recover.go b/middleware/recover/recover.go index 9208093f..0f0a614c 100644 --- a/middleware/recover/recover.go +++ b/middleware/recover/recover.go @@ -3,8 +3,8 @@ package recover import ( "fmt" + "net/http/httputil" "runtime" - "strconv" "github.com/kataras/iris/v12/context" ) @@ -14,13 +14,8 @@ func init() { } func getRequestLogs(ctx *context.Context) string { - var status, ip, method, path string - status = strconv.Itoa(ctx.GetStatusCode()) - path = ctx.Path() - method = ctx.Method() - ip = ctx.RemoteAddr() - // the date should be logged by iris' Logger, so we skip them - return fmt.Sprintf("%v %s %s %s", status, path, method, ip) + rawReq, _ := httputil.DumpRequest(ctx.Request(), false) + return string(rawReq) } // New returns a new recover middleware, @@ -30,7 +25,7 @@ func New() context.Handler { return func(ctx *context.Context) { defer func() { if err := recover(); err != nil { - if ctx.IsStopped() { + if ctx.IsStopped() { // handled by other middleware. return }