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:
Gerasimos (Makis) Maropoulos 2020-05-02 17:46:17 +03:00
parent 1e1d8a4855
commit dbd6fcd2d7
8 changed files with 223 additions and 105 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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)
}

View File

@ -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",

View File

@ -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
View File

@ -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.

View File

@ -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()