mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
add Context.SendFileWithRate, ServeFileWithRate and ServeContentWithRate
as requested at: https://github.com/kataras/iris/issues/1493 Former-commit-id: 7783fde04b4247056e6309e7ec1df27f027dc655
This commit is contained in:
parent
1e1d8a4855
commit
dbd6fcd2d7
44
HISTORY.md
44
HISTORY.md
|
@ -394,31 +394,33 @@ Other Improvements:
|
|||
|
||||
New Context Methods:
|
||||
|
||||
- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
|
||||
- `context.IsGRPC() bool` reports whether the request came from a gRPC client
|
||||
- `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
|
||||
- `context.StopWithStatus(int)` stops the handlers chain and writes the status code
|
||||
- `context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
|
||||
- `context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
|
||||
- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
|
||||
- `context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
|
||||
- `context.Protobuf(proto.Message)` sends protobuf to the client
|
||||
- `context.MsgPack(interface{})` sends msgpack format data to the client
|
||||
- `context.ReadProtobuf(ptr)` binds request body to a proto message
|
||||
- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
|
||||
- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
|
||||
- `context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
|
||||
- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
|
||||
- `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)`
|
||||
- `context.Controller() reflect.Value` returns the current MVC Controller value.
|
||||
- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client.
|
||||
- `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
|
||||
- `Context.IsGRPC() bool` reports whether the request came from a gRPC client
|
||||
- `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
|
||||
- `Context.StopWithStatus(int)` stops the handlers chain and writes the status code
|
||||
- `Context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
|
||||
- `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
|
||||
- `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
|
||||
- `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
|
||||
- `Context.Protobuf(proto.Message)` sends protobuf to the client
|
||||
- `Context.MsgPack(interface{})` sends msgpack format data to the client
|
||||
- `Context.ReadProtobuf(ptr)` binds request body to a proto message
|
||||
- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
|
||||
- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
|
||||
- `Context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
|
||||
- `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
|
||||
- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
|
||||
- `Context.Controller() reflect.Value` returns the current MVC Controller value.
|
||||
|
||||
Breaking Changes:
|
||||
|
||||
Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See <https://go-review.googlesource.com/c/go/+/186927/>.
|
||||
|
||||
- Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See <https://go-review.googlesource.com/c/go/+/186927/>.
|
||||
- Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.Gzip)`) or a prior call to `Context.Gzip(true)` will enable gzip compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed).
|
||||
- `Context.ServeContent` no longer returns an error, see `ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` new methods too.
|
||||
- `route.Trace() string` changed to `route.Trace(w io.Writer)`, to achieve the same result just pass a `bytes.Buffer`
|
||||
- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449))
|
||||
- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343))
|
||||
- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)).
|
||||
- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)).
|
||||
- `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead
|
||||
- **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead.
|
||||
|
||||
|
|
|
@ -237,7 +237,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
|
|||
- [Basic](file-server/basic/main.go)
|
||||
- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go)
|
||||
- [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go)
|
||||
- [Send/Force-Download Files](file-server/send-files/main.go)
|
||||
- [Send/Force-Download Files](file-server/send-files/main.go) **UPDATED**
|
||||
- Single Page Applications
|
||||
* [single Page Application](file-server/single-page-application/basic/main.go)
|
||||
* [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go)
|
||||
|
@ -246,7 +246,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
|
|||
### How to Read from `context.Request() *http.Request`
|
||||
|
||||
- [Read JSON](http_request/read-json/main.go)
|
||||
* [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDaTE**
|
||||
* [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDATED**
|
||||
- [Read XML](http_request/read-xml/main.go)
|
||||
- [Read MsgPack](http_request/read-msgpack/main.go) **NEW**
|
||||
- [Read YAML](http_request/read-yaml/main.go)
|
||||
|
|
Binary file not shown.
|
@ -6,11 +6,26 @@ import (
|
|||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.Logger().SetLevel("debug")
|
||||
|
||||
app.Get("/", func(ctx iris.Context) {
|
||||
file := "./files/first.zip"
|
||||
ctx.SendFile(file, "c.zip")
|
||||
})
|
||||
app.Get("/", download)
|
||||
app.Get("/download", downloadWithRateLimit)
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func download(ctx iris.Context) {
|
||||
src := "./files/first.zip"
|
||||
ctx.SendFile(src, "client.zip")
|
||||
}
|
||||
|
||||
func downloadWithRateLimit(ctx iris.Context) {
|
||||
// REPLACE THAT WITH A BIG LOCAL FILE OF YOUR OWN.
|
||||
src := "./files/first.zip"
|
||||
dest := "" /* optionally, keep it empty to resolve the filename based on the "src" */
|
||||
|
||||
// Limit download speed to ~50Kb/s with a burst of 100KB.
|
||||
limit := 50.0 * iris.KB
|
||||
burst := 100 * iris.KB
|
||||
ctx.SendFileWithRate(src, dest, limit, burst)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ import "github.com/kataras/iris/v12"
|
|||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
// app.Use(iris.Gzip)
|
||||
// func(ctx iris.Context) { ctx.Gzip(true/false)}
|
||||
// OR:
|
||||
app.Get("/", func(ctx iris.Context) {
|
||||
ctx.WriteGzip([]byte("Hello World!"))
|
||||
ctx.Header("X-Custom",
|
||||
|
|
|
@ -2,6 +2,7 @@ package context
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
stdContext "context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
|
@ -35,6 +36,7 @@ import (
|
|||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"golang.org/x/time/rate"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
@ -916,31 +918,57 @@ type Context interface {
|
|||
// | Serve files |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
// ServeContent serves content, headers are autoset
|
||||
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
|
||||
// ServeContent replies to the request using the content in the
|
||||
// provided ReadSeeker. The main benefit of ServeContent over io.Copy
|
||||
// is that it handles Range requests properly, sets the MIME type, and
|
||||
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
|
||||
// and If-Range requests.
|
||||
//
|
||||
// If the response's Content-Type header is not set, ServeContent
|
||||
// first tries to deduce the type from name's file extension.
|
||||
//
|
||||
// You can define your own "Content-Type" with `context#ContentType`, before this function call.
|
||||
// The name is otherwise unused; in particular it can be empty and is
|
||||
// never sent in the response.
|
||||
//
|
||||
// This function doesn't support resuming (by range),
|
||||
// use ctx.SendFile or router's `HandleDir` instead.
|
||||
ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error
|
||||
// ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead)
|
||||
// receives two parameters
|
||||
// filename/path (string)
|
||||
// gzipCompression (bool)
|
||||
// If modtime is not the zero time or Unix epoch, ServeContent
|
||||
// includes it in a Last-Modified header in the response. If the
|
||||
// request includes an If-Modified-Since header, ServeContent uses
|
||||
// modtime to decide whether the content needs to be sent at all.
|
||||
//
|
||||
// You can define your own "Content-Type" with `context#ContentType`, before this function call.
|
||||
// The content's Seek method must work: ServeContent uses
|
||||
// a seek to the end of the content to determine its size.
|
||||
//
|
||||
// This function doesn't support resuming (by range),
|
||||
// use ctx.SendFile or router's `HandleDir` instead.
|
||||
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
|
||||
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
|
||||
//
|
||||
// Use it when you want to serve dynamic files to the client.
|
||||
ServeFile(filename string, gzipCompression bool) error
|
||||
// SendFile sends file for force-download to the client
|
||||
// Note that *os.File implements the io.ReadSeeker interface.
|
||||
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||
ServeContent(content io.ReadSeeker, filename string, modtime time.Time)
|
||||
// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
|
||||
// and though writing the "content" to the client.
|
||||
ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int)
|
||||
// ServeFile replies to the request with the contents of the named
|
||||
// file or directory.
|
||||
//
|
||||
// Use this instead of ServeFile to 'force-download' bigger files to the client.
|
||||
// If the provided file or directory name is a relative path, it is
|
||||
// interpreted relative to the current directory and may ascend to
|
||||
// parent directories. If the provided name is constructed from user
|
||||
// input, it should be sanitized before calling `ServeFile`.
|
||||
//
|
||||
// Use it when you want to serve assets like css and javascript files.
|
||||
// If client should confirm and save the file use the `SendFile` instead.
|
||||
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||
ServeFile(filename string) error
|
||||
// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
|
||||
// and though writing the file to the client.
|
||||
ServeFileWithRate(filename string, limit float64, burst int) error
|
||||
// SendFile sends a file as an attachment, that is downloaded and saved locally from client.
|
||||
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||
// Use `ServeFile` if a file should be served as a page asset instead.
|
||||
SendFile(filename string, destinationName string) error
|
||||
// SendFileWithRate same as `SendFile` but it can throttle the speed of reading
|
||||
// and though writing the file to the client.
|
||||
SendFileWithRate(src, destName string, limit float64, burst int) error
|
||||
|
||||
// +------------------------------------------------------------+
|
||||
// | Cookies |
|
||||
|
@ -4544,65 +4572,135 @@ func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder {
|
|||
// | Serve files |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
// ServeContent serves content, headers are autoset
|
||||
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
|
||||
// ServeContent replies to the request using the content in the
|
||||
// provided ReadSeeker. The main benefit of ServeContent over io.Copy
|
||||
// is that it handles Range requests properly, sets the MIME type, and
|
||||
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
|
||||
// and If-Range requests.
|
||||
//
|
||||
// You can define your own "Content-Type" header also, after this function call
|
||||
// Doesn't implements resuming (by range), use ctx.SendFile instead
|
||||
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
|
||||
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
|
||||
ctx.WriteNotModified()
|
||||
return nil
|
||||
// If the response's Content-Type header is not set, ServeContent
|
||||
// first tries to deduce the type from name's file extension.
|
||||
//
|
||||
// The name is otherwise unused; in particular it can be empty and is
|
||||
// never sent in the response.
|
||||
//
|
||||
// If modtime is not the zero time or Unix epoch, ServeContent
|
||||
// includes it in a Last-Modified header in the response. If the
|
||||
// request includes an If-Modified-Since header, ServeContent uses
|
||||
// modtime to decide whether the content needs to be sent at all.
|
||||
//
|
||||
// The content's Seek method must work: ServeContent uses
|
||||
// a seek to the end of the content to determine its size.
|
||||
//
|
||||
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
|
||||
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
|
||||
//
|
||||
// Note that *os.File implements the io.ReadSeeker interface.
|
||||
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time) {
|
||||
ctx.ServeContentWithRate(content, filename, modtime, 0, 0)
|
||||
}
|
||||
|
||||
// rateReadSeeker is a io.ReadSeeker that is rate limited by
|
||||
// the given token bucket. Each token in the bucket
|
||||
// represents one byte. See "golang.org/x/time/rate" package.
|
||||
type rateReadSeeker struct {
|
||||
io.ReadSeeker
|
||||
ctx stdContext.Context
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (rs *rateReadSeeker) Read(buf []byte) (int, error) {
|
||||
n, err := rs.ReadSeeker.Read(buf)
|
||||
if n <= 0 {
|
||||
return n, err
|
||||
}
|
||||
rs.limiter.WaitN(rs.ctx, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
|
||||
// and though writing the "content" to the client.
|
||||
func (ctx *context) ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) {
|
||||
if limit > 0 {
|
||||
content = &rateReadSeeker{
|
||||
ReadSeeker: content,
|
||||
ctx: ctx.request.Context(),
|
||||
limiter: rate.NewLimiter(rate.Limit(limit), burst),
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.GetContentType() == "" {
|
||||
ctx.ContentType(filename)
|
||||
}
|
||||
|
||||
ctx.SetLastModified(modtime)
|
||||
var out io.Writer
|
||||
if gzipCompression && ctx.ClientSupportsGzip() {
|
||||
AddGzipHeaders(ctx.writer)
|
||||
|
||||
gzipWriter := acquireGzipWriter(ctx.writer)
|
||||
defer releaseGzipWriter(gzipWriter)
|
||||
out = gzipWriter
|
||||
} else {
|
||||
out = ctx.writer
|
||||
}
|
||||
_, err := io.Copy(out, content)
|
||||
return err ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api?
|
||||
http.ServeContent(ctx.writer, ctx.request, filename, modtime, content)
|
||||
}
|
||||
|
||||
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)
|
||||
// receives two parameters
|
||||
// filename/path (string)
|
||||
// gzipCompression (bool)
|
||||
// ServeFile replies to the request with the contents of the named
|
||||
// file or directory.
|
||||
//
|
||||
// You can define your own "Content-Type" header also, after this function call
|
||||
// This function doesn't implement resuming (by range), use ctx.SendFile instead
|
||||
// If the provided file or directory name is a relative path, it is
|
||||
// interpreted relative to the current directory and may ascend to
|
||||
// parent directories. If the provided name is constructed from user
|
||||
// input, it should be sanitized before calling `ServeFile`.
|
||||
//
|
||||
// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile.
|
||||
func (ctx *context) ServeFile(filename string, gzipCompression bool) error {
|
||||
// Use it when you want to serve assets like css and javascript files.
|
||||
// If client should confirm and save the file use the `SendFile` instead.
|
||||
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||
func (ctx *context) ServeFile(filename string) error {
|
||||
return ctx.ServeFileWithRate(filename, 0, 0)
|
||||
}
|
||||
|
||||
// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
|
||||
// and though writing the file to the client.
|
||||
func (ctx *context) ServeFileWithRate(filename string, limit float64, burst int) error {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d", http.StatusNotFound)
|
||||
ctx.StatusCode(http.StatusNotFound)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, _ := f.Stat()
|
||||
if fi.IsDir() {
|
||||
return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression)
|
||||
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
code := http.StatusInternalServerError
|
||||
if os.IsNotExist(err) {
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
|
||||
if os.IsPermission(err) {
|
||||
code = http.StatusForbidden
|
||||
}
|
||||
|
||||
ctx.StatusCode(code)
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression)
|
||||
if st.IsDir() {
|
||||
return ctx.ServeFile(path.Join(filename, "index.html"))
|
||||
}
|
||||
|
||||
ctx.ServeContentWithRate(f, st.Name(), st.ModTime(), limit, burst)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendFile sends file for force-download to the client
|
||||
//
|
||||
// Use this instead of ServeFile to 'force-download' bigger files to the client.
|
||||
func (ctx *context) SendFile(filename string, destinationName string) error {
|
||||
ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destinationName)
|
||||
return ctx.ServeFile(filename, false)
|
||||
// SendFile sends a file as an attachment, that is downloaded and saved locally from client.
|
||||
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||
// Use `ServeFile` if a file should be served as a page asset instead.
|
||||
func (ctx *context) SendFile(src string, destName string) error {
|
||||
return ctx.SendFileWithRate(src, destName, 0, 0)
|
||||
}
|
||||
|
||||
// SendFileWithRate same as `SendFile` but it can throttle the speed of reading
|
||||
// and though writing the file to the client.
|
||||
func (ctx *context) SendFileWithRate(src, destName string, limit float64, burst int) error {
|
||||
if destName == "" {
|
||||
destName = filepath.Base(src)
|
||||
}
|
||||
|
||||
ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destName)
|
||||
return ctx.ServeFileWithRate(src, limit, burst)
|
||||
}
|
||||
|
||||
// +------------------------------------------------------------+
|
||||
|
|
11
iris.go
11
iris.go
|
@ -591,6 +591,17 @@ const (
|
|||
ReferrerGoogleAdwords = context.ReferrerGoogleAdwords
|
||||
)
|
||||
|
||||
// Byte unit helpers.
|
||||
const (
|
||||
B = 1 << (10 * iota)
|
||||
KB
|
||||
MB
|
||||
GB
|
||||
TB
|
||||
PB
|
||||
EB
|
||||
)
|
||||
|
||||
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions
|
||||
// can access the host created by `app.Run`,
|
||||
// they're being executed when application is ready to being served to the public.
|
||||
|
|
|
@ -50,10 +50,11 @@ type (
|
|||
Limiter struct {
|
||||
clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field.
|
||||
exceedHandler context.Handler // when too many requests.
|
||||
limit rate.Limit
|
||||
burstSize int
|
||||
|
||||
clients map[string]*Client
|
||||
mu sync.RWMutex // mutex for clients.
|
||||
pool *sync.Pool // object pool for clients.
|
||||
}
|
||||
|
||||
Client struct {
|
||||
|
@ -68,14 +69,10 @@ type (
|
|||
const Inf = math.MaxFloat64
|
||||
|
||||
func Limit(limit float64, burst int, options ...Option) context.Handler {
|
||||
rateLimit := rate.Limit(limit)
|
||||
|
||||
l := &Limiter{
|
||||
clients: make(map[string]*Client),
|
||||
pool: &sync.Pool{New: func() interface{} {
|
||||
return &Client{limiter: rate.NewLimiter(rateLimit, burst)}
|
||||
}},
|
||||
|
||||
clients: make(map[string]*Client),
|
||||
limit: rate.Limit(limit),
|
||||
burstSize: burst,
|
||||
exceedHandler: func(ctx context.Context) {
|
||||
ctx.StopWithStatus(429) // Too Many Requests.
|
||||
},
|
||||
|
@ -88,21 +85,10 @@ func Limit(limit float64, burst int, options ...Option) context.Handler {
|
|||
return l.serveHTTP
|
||||
}
|
||||
|
||||
func (l *Limiter) acquire() *Client {
|
||||
return l.pool.Get().(*Client)
|
||||
}
|
||||
|
||||
func (l *Limiter) release(client *Client) {
|
||||
client.IP = ""
|
||||
client.Data = nil
|
||||
l.pool.Put(client)
|
||||
}
|
||||
|
||||
func (l *Limiter) Purge(condition func(*Client) bool) {
|
||||
l.mu.Lock()
|
||||
for ip, client := range l.clients {
|
||||
if condition(client) {
|
||||
l.release(client)
|
||||
delete(l.clients, ip)
|
||||
}
|
||||
}
|
||||
|
@ -116,12 +102,15 @@ func (l *Limiter) serveHTTP(ctx context.Context) {
|
|||
l.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
client = l.acquire()
|
||||
client.IP = ip
|
||||
client = &Client{
|
||||
limiter: rate.NewLimiter(l.limit, l.burstSize),
|
||||
IP: ip,
|
||||
}
|
||||
|
||||
if l.clientDataFunc != nil {
|
||||
client.Data = l.clientDataFunc(ctx)
|
||||
}
|
||||
|
||||
// if l.store(ctx, client) {
|
||||
// ^ no, let's keep it simple.
|
||||
l.mu.Lock()
|
||||
|
|
Loading…
Reference in New Issue
Block a user