-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from mazrean/doc/readme
write README
- Loading branch information
Showing
14 changed files
with
730 additions
and
19 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 |
---|---|---|
@@ -1,6 +1,151 @@ | ||
# Formstream | ||
# FormStream | ||
|
||
[![GitHub release](https://img.shields.io/github/release/mazrean/formstream.svg)](https://github.com/mazrean/formstream/releases/) | ||
![CI main](https://github.com/mazrean/formstream/actions/workflows/ci.yaml/badge.svg) | ||
[![codecov](https://codecov.io/gh/mazrean/formstream/branch/master/graph/badge.svg)](https://codecov.io/gh/mazrean/formstream) | ||
[![Go Reference](https://pkg.go.dev/badge/github.com/mazrean/formstream.svg)](https://pkg.go.dev/github.com/mazrean/formstream) | ||
|
||
FormStream is a Golang streaming parser for multipart data, primarily used in web form submissions and file uploads. | ||
|
||
## Features | ||
|
||
- Provides a streaming parser, eliminating the need to store entire files in memory or on disk in most cases. | ||
- Boasts extremely low memory usage. | ||
- Delivers high performance, significantly faster than traditional methods. | ||
|
||
## Benchmarks | ||
|
||
Across all file sizes, FormStream outperforms the [`mime/multipart`](https://pkg.go.dev/mime/multipart) in both speed and memory efficiency. | ||
|
||
<details> | ||
<summary>Testing Environment</summary> | ||
|
||
- OS: | ||
- CPU: | ||
- RAM: | ||
- Disk: | ||
- Go version: | ||
</details> | ||
|
||
![](./docs/images/memory.png) | ||
![](./docs/images/time.png) | ||
|
||
> [!NOTE] | ||
> FormStream excels in speed by employing a stream for parsing multipart data that meets specific conditions (as shown in the `FastPath` on the graph). | ||
> It remains significantly efficient even under less ideal conditions (`SlowPath` on the graph), marginally outperforming [`mime/multipart`](https://pkg.go.dev/mime/multipart). | ||
> For more details, see [Technical Overview](./#technical-overview). | ||
## Installation | ||
|
||
```sh | ||
go get github.com/mazrean/formstream@latest | ||
``` | ||
|
||
## Usage | ||
|
||
### Basic 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 | ||
parser, err := formstream.NewParser(r) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = parser.Register("icon", func(r io.Reader, header formstream.Header) error { | ||
name, _, _ := parser.Value("name") | ||
password, _, _ := parser.Value("password") | ||
|
||
return saveUser(r.Context(), name, password, r) | ||
}, formstream.WithRequiredPart("name"), formstream.WithRequiredPart("password")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = parser.Parse() | ||
if err != nil { | ||
return err | ||
} | ||
``` | ||
|
||
### Integration with Web Frameworks | ||
|
||
FormStream offers wrappers for popular web frameworks: | ||
|
||
|Framework|Integration Package| | ||
|-|-| | ||
|[net/http](https://pkg.go.dev/net/http)|[httpform](./http)| | ||
|[Echo](https://echo.labstack.com/)|[echoform](./echo)| | ||
|[Gin](https://gin-gonic.com/)|[ginform](./gin)| | ||
|
||
## Technical Overview | ||
|
||
FormStream introduces a more efficient method for processing multipart data. | ||
|
||
### Understanding Multipart Data | ||
|
||
Multipart data is organized with defined boundaries separating each segment. Here's an example: | ||
|
||
```text | ||
--boundary | ||
Content-Disposition: form-data; name="description" | ||
file description | ||
--boundary | ||
Content-Disposition: form-data; name="file"; filename="large.png" | ||
Content-Type: image/png | ||
large png data... | ||
--boundary-- | ||
``` | ||
|
||
For large files, streaming the data is vital for efficient memory usage. In the example above, streaming is made possible by sequentially processing each part from the beginning, which can be achieved using the [`(*Reader).NextPart`](https://pkg.go.dev/mime/multipart#Reader.NextPart) method in the [`mime/multipart`](https://pkg.go.dev/mime/multipart) package. | ||
|
||
### Alternative Parsing Method | ||
|
||
The [`mime/multipart`](https://pkg.go.dev/mime/multipart) package also includes the [`(*Reader).ReadForm`](https://pkg.go.dev/mime/multipart#Reader.ReadForm) method. Unlike streaming, this method stores data temporarily in memory or on a file, leading to slower processing. It's widely used in frameworks like `net/http`, `Echo`, and `Gin` due to its ability to handle parts in any order. For instance: | ||
|
||
```text | ||
--boundary | ||
Content-Disposition: form-data; name="file"; filename="large.png" | ||
Content-Type: image/png | ||
large png data... | ||
--boundary | ||
Content-Disposition: form-data; name="description" | ||
file description | ||
--boundary-- | ||
``` | ||
|
||
With [`(*Reader).NextPart`](https://pkg.go.dev/mime/multipart#Reader.NextPart), processing strictly follows sequential order, making it challenging to handle such data where later parts contain information necessary for processing earlier ones. | ||
|
||
### Efficient Processing Strategies | ||
|
||
Optimal multipart handling strategies include: | ||
- Stream processing with [`(*Reader).NextPart`](https://pkg.go.dev/mime/multipart#Reader.NextPart) when all necessary data is immediately available. | ||
- Temporarily storing data on disk or memory, then processing it with [`(*Reader).ReadForm`](https://pkg.go.dev/mime/multipart#Reader.ReadForm) when needed. | ||
|
||
### Advantages of FormStream | ||
|
||
FormStream enhances this process. It outpaces the [`(*Reader).ReadForm`](https://pkg.go.dev/mime/multipart#Reader.ReadForm) method and, unlike [`(*Reader).NextPart`](https://pkg.go.dev/mime/multipart#Reader.NextPart), can handle multipart data in any order. This adaptability makes FormStream suitable for a range of multipart data scenarios. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.NewParser(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 |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package echo | ||
package echoform | ||
|
||
import ( | ||
"io" | ||
|
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,101 @@ | ||
package echoform_test | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/labstack/echo/v4" | ||
"github.com/mazrean/formstream" | ||
echoform "github.com/mazrean/formstream/echo" | ||
) | ||
|
||
func TestExample(t *testing.T) { | ||
e := echo.New() | ||
|
||
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(` | ||
--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--`)) | ||
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 != "mazrean" { | ||
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.NewParser(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) | ||
} | ||
|
||
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
Oops, something went wrong.