-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: file handling / multi-part form helpers with echo wrappers (#9)
* add some file handling functions, other required components for evaluating multipart form http requests * add comments to missing exported functions
- Loading branch information
Showing
11 changed files
with
394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Wrapper for [echo](https://echo.labstack.com/) | ||
|
||
## Usage | ||
|
||
<details> | ||
<summary>Example data</summary> | ||
|
||
```text | ||
--boundary | ||
Content-Disposition: form-data; name="name" | ||
mazrean | ||
--boundary | ||
Content-Disposition: form-data; name="password" | ||
password | ||
--boundary | ||
Content-Disposition: form-data; name="icon"; filename="icon.png" | ||
Content-Type: image/png | ||
icon contents | ||
--boundary-- | ||
``` | ||
</details> | ||
|
||
```go | ||
func createUserHandler(c echo.Context) error { | ||
parser, err := echoform.NewFormParser(c) | ||
if err != nil { | ||
return c.NoContent(http.StatusBadRequest) | ||
} | ||
|
||
err = parser.Register("icon", func(r io.Reader, header formstream.Header) error { | ||
name, _, _ := parser.Value("name") | ||
password, _, _ := parser.Value("password") | ||
|
||
return saveUser(c.Request().Context(), name, password, r) | ||
}, formstream.WithRequiredPart("name"), formstream.WithRequiredPart("password")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = parser.Parse() | ||
if err != nil { | ||
return c.NoContent(http.StatusBadRequest) | ||
} | ||
|
||
return c.NoContent(http.StatusCreated) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package echoform | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
echo "github.com/theopenlane/echox" | ||
) | ||
|
||
// EchoContextAdapter acts as an adapter for an `echo.Context` object. It provides methods to interact with the underlying | ||
// `echo.Context` object and extract information such as deadline, done channel, error, and values | ||
// associated with specific keys from the context. The struct is used to enhance the functionality of | ||
// the `echo.Context` object by providing additional methods and capabilities | ||
type EchoContextAdapter struct { | ||
c echo.Context | ||
} | ||
|
||
// NewEchoContextAdapter takes echo.Context as a parameter and returns a pointer to | ||
// a new EchoContextAdapter struct initialized with the provided echo.Context | ||
func NewEchoContextAdapter(c echo.Context) *EchoContextAdapter { | ||
return &EchoContextAdapter{c: c} | ||
} | ||
|
||
// Deadline represents the time when the request should be completed | ||
// deadline returns two values: deadline, which is the deadline time, and ok, indicating if a deadline is set or not | ||
func (a *EchoContextAdapter) Deadline() (deadline time.Time, ok bool) { | ||
return a.c.Request().Context().Deadline() | ||
} | ||
|
||
// Done channel is used to receive a signal when the request context associated with the EchoContextAdapter is done or canceled | ||
func (a *EchoContextAdapter) Done() <-chan struct{} { | ||
return a.c.Request().Context().Done() | ||
} | ||
|
||
// Err handles if an error occurred during the processing of the request | ||
func (a *EchoContextAdapter) Err() error { | ||
return a.c.Request().Context().Err() | ||
} | ||
|
||
// Value implements the Value method of the context.Context interface | ||
// used to retrieve a value associated with a specific key from the context | ||
func (a *EchoContextAdapter) Value(key interface{}) interface{} { | ||
return a.c.Get(fmt.Sprintf("%v", key)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package echoform |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package echoform | ||
|
||
import ( | ||
"errors" | ||
"io" | ||
"mime" | ||
"net/http" | ||
|
||
"github.com/mazrean/formstream" | ||
echo "github.com/theopenlane/echox" | ||
) | ||
|
||
type FormParser struct { | ||
*formstream.Parser | ||
reader io.Reader | ||
} | ||
|
||
// NewFormParser creates a new multipart form parser | ||
func NewFormParser(c echo.Context, options ...formstream.ParserOption) (*FormParser, error) { | ||
contentType := c.Request().Header.Get("Content-Type") | ||
|
||
d, params, err := mime.ParseMediaType(contentType) | ||
if err != nil || d != "multipart/form-data" { | ||
return nil, http.ErrNotMultipart | ||
} | ||
|
||
boundary, ok := params["boundary"] | ||
if !ok { | ||
return nil, http.ErrMissingBoundary | ||
} | ||
|
||
return &FormParser{ | ||
Parser: formstream.NewParser(boundary, options...), | ||
reader: c.Request().Body, | ||
}, nil | ||
} | ||
|
||
// Parse parses the request body; it returns the echo.HTTPError if the hook function returns an echo.HTTPError | ||
func (p *FormParser) Parse() error { | ||
err := p.Parser.Parse(p.reader) | ||
|
||
var httpErr *echo.HTTPError | ||
if errors.As(err, &httpErr) { | ||
return httpErr | ||
} | ||
|
||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package echoform_test | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/mazrean/formstream" | ||
echo "github.com/theopenlane/echox" | ||
|
||
echoform "github.com/theopenlane/httpsling/echo" | ||
) | ||
|
||
func TestExample(t *testing.T) { | ||
e := echo.New() | ||
|
||
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(` | ||
--boundary | ||
Content-Disposition: form-data; name="name" | ||
mitb | ||
--boundary | ||
Content-Disposition: form-data; name="password" | ||
password | ||
--boundary | ||
Content-Disposition: form-data; name="icon"; filename="icon.png" | ||
Content-Type: image/png | ||
icon contents | ||
--boundary--`)) | ||
req.Header.Set(echo.HeaderContentType, "multipart/form-data; boundary=boundary") | ||
|
||
rec := httptest.NewRecorder() | ||
c := e.NewContext(req, rec) | ||
|
||
err := createUserHandler(c) | ||
if err != nil { | ||
t.Fatalf("failed to create user: %s\n", err) | ||
return | ||
} | ||
|
||
if user.name != "mitb" { | ||
t.Errorf("user name is wrong: expected: mazrean, actual: %s\n", user.name) | ||
} | ||
|
||
if user.password != "password" { | ||
t.Errorf("user password is wrong: expected: password, actual: %s\n", user.password) | ||
} | ||
|
||
if user.icon != "icon contents" { | ||
t.Errorf("user icon is wrong: expected: icon contents, actual: %s\n", user.icon) | ||
} | ||
} | ||
|
||
func createUserHandler(c echo.Context) error { | ||
parser, err := echoform.NewFormParser(c) | ||
if err != nil { | ||
return c.NoContent(http.StatusBadRequest) | ||
} | ||
|
||
err = parser.Register("icon", func(r io.Reader, _ formstream.Header) error { | ||
name, _, _ := parser.Value("name") | ||
password, _, _ := parser.Value("password") | ||
|
||
return saveUser(c.Request().Context(), name, password, r) | ||
}, formstream.WithRequiredPart("name"), formstream.WithRequiredPart("password")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = parser.Parse() | ||
if err != nil { | ||
return c.NoContent(http.StatusBadRequest) | ||
} | ||
|
||
return c.NoContent(http.StatusCreated) | ||
} | ||
|
||
var ( | ||
user = struct { | ||
name string | ||
password string | ||
icon string | ||
}{} | ||
) | ||
|
||
func saveUser(_ context.Context, name string, password string, iconReader io.Reader) error { | ||
user.name = name | ||
user.password = password | ||
|
||
sb := strings.Builder{} | ||
|
||
_, err := io.Copy(&sb, iconReader) | ||
if err != nil { | ||
return fmt.Errorf("failed to copy: %w", err) | ||
} | ||
|
||
user.icon = sb.String() | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package httpsling | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// Files is a map of form field names to a slice of files | ||
type Files map[string][]File | ||
|
||
// File represents a file that has been sent in an http request | ||
type File struct { | ||
// FieldName denotes the field from the multipart form | ||
FieldName string `json:"field_name,omitempty"` | ||
// OriginalName is he name of the file from the client side / which was sent in the request | ||
OriginalName string `json:"original_name,omitempty"` | ||
// MimeType of the uploaded file | ||
MimeType string `json:"mime_type,omitempty"` | ||
// Size in bytes of the uploaded file | ||
Size int64 `json:"size,omitempty"` | ||
} | ||
|
||
// ValidationFunc is a type that can be used to dynamically validate a file | ||
type ValidationFunc func(f File) error | ||
|
||
// ErrResponseHandler is a custom error that should be used to handle errors when an upload fails | ||
type ErrResponseHandler func(error) http.HandlerFunc | ||
|
||
// NameGeneratorFunc allows you alter the name of the file before it is ultimately uploaded and stored | ||
type NameGeneratorFunc func(s string) string | ||
|
||
// FilesFromContext returns all files that have been uploaded during the request | ||
func FilesFromContext(r *http.Request, key string) (Files, error) { | ||
files, ok := r.Context().Value(key).(Files) | ||
if !ok { | ||
return nil, ErrNoFilesUploaded | ||
} | ||
|
||
return files, nil | ||
} | ||
|
||
// FilesFromContextWithKey returns all files that have been uploaded during the request | ||
// and sorts by the provided form field | ||
func FilesFromContextWithKey(r *http.Request, key string) ([]File, error) { | ||
files, ok := r.Context().Value(key).(Files) | ||
if !ok { | ||
return nil, ErrNoFilesUploaded | ||
} | ||
|
||
return files[key], nil | ||
} | ||
|
||
// MimeTypeValidator makes sure we only accept a valid mimetype. | ||
// It takes in an array of supported mimes | ||
func MimeTypeValidator(validMimeTypes ...string) ValidationFunc { | ||
return func(f File) error { | ||
for _, mimeType := range validMimeTypes { | ||
if strings.EqualFold(strings.ToLower(mimeType), f.MimeType) { | ||
return nil | ||
} | ||
} | ||
|
||
return fmt.Errorf("%w: %s", ErrUnsupportedMimeType, f.MimeType) | ||
} | ||
} | ||
|
||
// ChainValidators returns a validator that accepts multiple validating criteras | ||
func ChainValidators(validators ...ValidationFunc) ValidationFunc { | ||
return func(f File) error { | ||
for _, validator := range validators { | ||
if err := validator(f); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.