mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +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:
|
New Context Methods:
|
||||||
|
|
||||||
- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
|
- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client.
|
||||||
- `context.IsGRPC() bool` reports whether the request came from a gRPC client
|
- `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
|
||||||
- `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
|
- `Context.IsGRPC() bool` reports whether the request came from a gRPC client
|
||||||
- `context.StopWithStatus(int)` stops the handlers chain and writes the status code
|
- `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
|
||||||
- `context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
|
- `Context.StopWithStatus(int)` stops the handlers chain and writes the status code
|
||||||
- `context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
|
- `Context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
|
||||||
- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
|
- `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
|
||||||
- `context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
|
- `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
|
||||||
- `context.Protobuf(proto.Message)` sends protobuf to the client
|
- `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
|
||||||
- `context.MsgPack(interface{})` sends msgpack format data to the client
|
- `Context.Protobuf(proto.Message)` sends protobuf to the client
|
||||||
- `context.ReadProtobuf(ptr)` binds request body to a proto message
|
- `Context.MsgPack(interface{})` sends msgpack format data to the client
|
||||||
- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
|
- `Context.ReadProtobuf(ptr)` binds request body to a proto message
|
||||||
- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
|
- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
|
||||||
- `context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
|
- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
|
||||||
- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
|
- `Context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
|
||||||
- `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)`
|
- `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
|
||||||
- `context.Controller() reflect.Value` returns the current MVC Controller value.
|
- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
|
||||||
|
- `Context.Controller() reflect.Value` returns the current MVC Controller value.
|
||||||
|
|
||||||
Breaking Changes:
|
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`
|
- `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))
|
- `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#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
|
- `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.
|
- **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)
|
- [Basic](file-server/basic/main.go)
|
||||||
- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/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)
|
- [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 Applications
|
||||||
* [single Page Application](file-server/single-page-application/basic/main.go)
|
* [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)
|
* [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`
|
### How to Read from `context.Request() *http.Request`
|
||||||
|
|
||||||
- [Read JSON](http_request/read-json/main.go)
|
- [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 XML](http_request/read-xml/main.go)
|
||||||
- [Read MsgPack](http_request/read-msgpack/main.go) **NEW**
|
- [Read MsgPack](http_request/read-msgpack/main.go) **NEW**
|
||||||
- [Read YAML](http_request/read-yaml/main.go)
|
- [Read YAML](http_request/read-yaml/main.go)
|
||||||
|
|
Binary file not shown.
|
@ -6,11 +6,26 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
app.Logger().SetLevel("debug")
|
||||||
|
|
||||||
app.Get("/", func(ctx iris.Context) {
|
app.Get("/", download)
|
||||||
file := "./files/first.zip"
|
app.Get("/download", downloadWithRateLimit)
|
||||||
ctx.SendFile(file, "c.zip")
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Listen(":8080")
|
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() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
// app.Use(iris.Gzip)
|
||||||
|
// func(ctx iris.Context) { ctx.Gzip(true/false)}
|
||||||
|
// OR:
|
||||||
app.Get("/", func(ctx iris.Context) {
|
app.Get("/", func(ctx iris.Context) {
|
||||||
ctx.WriteGzip([]byte("Hello World!"))
|
ctx.WriteGzip([]byte("Hello World!"))
|
||||||
ctx.Header("X-Custom",
|
ctx.Header("X-Custom",
|
||||||
|
|
|
@ -2,6 +2,7 @@ package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
stdContext "context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -35,6 +36,7 @@ import (
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -916,31 +918,57 @@ type Context interface {
|
||||||
// | Serve files |
|
// | Serve files |
|
||||||
// +------------------------------------------------------------+
|
// +------------------------------------------------------------+
|
||||||
|
|
||||||
// ServeContent serves content, headers are autoset
|
// ServeContent replies to the request using the content in the
|
||||||
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
|
// 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),
|
// If modtime is not the zero time or Unix epoch, ServeContent
|
||||||
// use ctx.SendFile or router's `HandleDir` instead.
|
// includes it in a Last-Modified header in the response. If the
|
||||||
ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error
|
// request includes an If-Modified-Since header, ServeContent uses
|
||||||
// ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead)
|
// modtime to decide whether the content needs to be sent at all.
|
||||||
// receives two parameters
|
|
||||||
// filename/path (string)
|
|
||||||
// gzipCompression (bool)
|
|
||||||
//
|
//
|
||||||
// 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),
|
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
|
||||||
// use ctx.SendFile or router's `HandleDir` instead.
|
// 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.
|
// Note that *os.File implements the io.ReadSeeker interface.
|
||||||
ServeFile(filename string, gzipCompression bool) error
|
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
|
||||||
// SendFile sends file for force-download to the client
|
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
|
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 |
|
// | Cookies |
|
||||||
|
@ -4544,65 +4572,135 @@ func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder {
|
||||||
// | Serve files |
|
// | Serve files |
|
||||||
// +------------------------------------------------------------+
|
// +------------------------------------------------------------+
|
||||||
|
|
||||||
// ServeContent serves content, headers are autoset
|
// ServeContent replies to the request using the content in the
|
||||||
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
|
// 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
|
// If the response's Content-Type header is not set, ServeContent
|
||||||
// Doesn't implements resuming (by range), use ctx.SendFile instead
|
// first tries to deduce the type from name's file extension.
|
||||||
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
|
//
|
||||||
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
|
// The name is otherwise unused; in particular it can be empty and is
|
||||||
ctx.WriteNotModified()
|
// never sent in the response.
|
||||||
return nil
|
//
|
||||||
|
// 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() == "" {
|
if ctx.GetContentType() == "" {
|
||||||
ctx.ContentType(filename)
|
ctx.ContentType(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetLastModified(modtime)
|
http.ServeContent(ctx.writer, ctx.request, filename, modtime, content)
|
||||||
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?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)
|
// ServeFile replies to the request with the contents of the named
|
||||||
// receives two parameters
|
// file or directory.
|
||||||
// filename/path (string)
|
|
||||||
// gzipCompression (bool)
|
|
||||||
//
|
//
|
||||||
// You can define your own "Content-Type" header also, after this function call
|
// If the provided file or directory name is a relative path, it is
|
||||||
// This function doesn't implement resuming (by range), use ctx.SendFile instead
|
// 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.
|
// Use it when you want to serve assets like css and javascript files.
|
||||||
func (ctx *context) ServeFile(filename string, gzipCompression bool) error {
|
// 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)
|
f, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%d", http.StatusNotFound)
|
ctx.StatusCode(http.StatusNotFound)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
fi, _ := f.Stat()
|
|
||||||
if fi.IsDir() {
|
st, err := f.Stat()
|
||||||
return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression)
|
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
|
// 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 this instead of ServeFile to 'force-download' bigger files to the client.
|
// Use `ServeFile` if a file should be served as a page asset instead.
|
||||||
func (ctx *context) SendFile(filename string, destinationName string) error {
|
func (ctx *context) SendFile(src string, destName string) error {
|
||||||
ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destinationName)
|
return ctx.SendFileWithRate(src, destName, 0, 0)
|
||||||
return ctx.ServeFile(filename, false)
|
}
|
||||||
|
|
||||||
|
// 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
|
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
|
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions
|
||||||
// can access the host created by `app.Run`,
|
// can access the host created by `app.Run`,
|
||||||
// they're being executed when application is ready to being served to the public.
|
// they're being executed when application is ready to being served to the public.
|
||||||
|
|
|
@ -50,10 +50,11 @@ type (
|
||||||
Limiter struct {
|
Limiter struct {
|
||||||
clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field.
|
clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field.
|
||||||
exceedHandler context.Handler // when too many requests.
|
exceedHandler context.Handler // when too many requests.
|
||||||
|
limit rate.Limit
|
||||||
|
burstSize int
|
||||||
|
|
||||||
clients map[string]*Client
|
clients map[string]*Client
|
||||||
mu sync.RWMutex // mutex for clients.
|
mu sync.RWMutex // mutex for clients.
|
||||||
pool *sync.Pool // object pool for clients.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Client struct {
|
Client struct {
|
||||||
|
@ -68,14 +69,10 @@ type (
|
||||||
const Inf = math.MaxFloat64
|
const Inf = math.MaxFloat64
|
||||||
|
|
||||||
func Limit(limit float64, burst int, options ...Option) context.Handler {
|
func Limit(limit float64, burst int, options ...Option) context.Handler {
|
||||||
rateLimit := rate.Limit(limit)
|
|
||||||
|
|
||||||
l := &Limiter{
|
l := &Limiter{
|
||||||
clients: make(map[string]*Client),
|
clients: make(map[string]*Client),
|
||||||
pool: &sync.Pool{New: func() interface{} {
|
limit: rate.Limit(limit),
|
||||||
return &Client{limiter: rate.NewLimiter(rateLimit, burst)}
|
burstSize: burst,
|
||||||
}},
|
|
||||||
|
|
||||||
exceedHandler: func(ctx context.Context) {
|
exceedHandler: func(ctx context.Context) {
|
||||||
ctx.StopWithStatus(429) // Too Many Requests.
|
ctx.StopWithStatus(429) // Too Many Requests.
|
||||||
},
|
},
|
||||||
|
@ -88,21 +85,10 @@ func Limit(limit float64, burst int, options ...Option) context.Handler {
|
||||||
return l.serveHTTP
|
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) {
|
func (l *Limiter) Purge(condition func(*Client) bool) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
for ip, client := range l.clients {
|
for ip, client := range l.clients {
|
||||||
if condition(client) {
|
if condition(client) {
|
||||||
l.release(client)
|
|
||||||
delete(l.clients, ip)
|
delete(l.clients, ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,12 +102,15 @@ func (l *Limiter) serveHTTP(ctx context.Context) {
|
||||||
l.mu.RUnlock()
|
l.mu.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
client = l.acquire()
|
client = &Client{
|
||||||
client.IP = ip
|
limiter: rate.NewLimiter(l.limit, l.burstSize),
|
||||||
|
IP: ip,
|
||||||
|
}
|
||||||
|
|
||||||
if l.clientDataFunc != nil {
|
if l.clientDataFunc != nil {
|
||||||
client.Data = l.clientDataFunc(ctx)
|
client.Data = l.clientDataFunc(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if l.store(ctx, client) {
|
// if l.store(ctx, client) {
|
||||||
// ^ no, let's keep it simple.
|
// ^ no, let's keep it simple.
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user