Former-commit-id: f4a19eb83a28279782b8a75ee298b38c9e180157
This commit is contained in:
kataras 2017-08-12 08:49:00 +03:00
parent 71af9d7f45
commit e00cf383a2
6 changed files with 119 additions and 52 deletions

View File

@ -9,6 +9,17 @@ func main() {
app.Favicon("./assets/favicon.ico")
// enable gzip, optionally:
// if used before the `StaticXXX` handlers then
// the content byte range feature is gone.
// recommend: turn off for large files especially
// when server has low memory,
// turn on for medium-sized files
// or for large-sized files if they are zipped already,
// i.e "zippedDir/file.gz"
//
// app.Use(iris.Gzip)
// first parameter is the request path
// second is the system directory
//

View File

@ -767,6 +767,13 @@ var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) Handler {
}
}
// Gzip is a middleware which enables writing
// using gzip compression, if client supports.
var Gzip = func(ctx Context) {
ctx.Gzip(true)
ctx.Next()
}
// Map is just a shortcut of the map[string]interface{}.
type Map map[string]interface{}

View File

@ -99,31 +99,40 @@ func (w *GzipResponseWriter) EndResponse() {
w.ResponseWriter.EndResponse()
}
// Write compresses and writes that data to the underline response writer
// Write prepares the data write to the gzip writer and finally to its
// underline response writer, returns the uncompressed len(contents).
func (w *GzipResponseWriter) Write(contents []byte) (int, error) {
// save the contents to serve them (only gzip data here)
w.chunks = append(w.chunks, contents...)
return len(w.chunks), nil
}
// WriteNow compresses and writes that data to the underline response writer,
// returns the compressed written len.
//
// Use `WriteNow` instead of `Write`
// when you need to know the compressed written size before
// the `FlushResponse`, note that you can't post any new headers
// after that, so that information is not closed to the handler anymore.
func (w *GzipResponseWriter) WriteNow(contents []byte) (int, error) {
if w.disabled {
return w.ResponseWriter.Write(contents)
}
w.ResponseWriter.Header().Add(varyHeaderKey, "Accept-Encoding")
w.ResponseWriter.Header().Set(contentEncodingHeaderKey, "gzip")
// if not `WriteNow` but "Content-Length" header
// is exists, then delete it before `.Write`
// Content-Length should not be there.
// no, for now at least: w.ResponseWriter.Header().Del(contentLengthHeaderKey)
return w.gzipWriter.Write(contents)
}
// FlushResponse validates the response headers in order to be compatible with the gzip written data
// and writes the data to the underline ResponseWriter.
func (w *GzipResponseWriter) FlushResponse() {
if w.disabled {
w.ResponseWriter.Write(w.chunks)
// remove gzip headers: no need, we just add two of them if gzip was enabled, below
// headers := w.ResponseWriter.Header()
// headers[contentType] = nil
// headers["X-Content-Type-Options"] = nil
// headers[varyHeader] = nil
// headers[contentEncodingHeader] = nil
// headers[contentLength] = nil
} else {
// if it's not disable write all chunks gzip compressed with the correct response headers.
w.ResponseWriter.Header().Add(varyHeaderKey, "Accept-Encoding")
w.ResponseWriter.Header().Set(contentEncodingHeaderKey, "gzip")
w.gzipWriter.Write(w.chunks) // it writes to the underline ResponseWriter.
}
w.WriteNow(w.chunks)
w.ResponseWriter.FlushResponse()
}

View File

@ -645,15 +645,18 @@ func (rb *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route {
handler := func(ctx context.Context) {
h(ctx)
// re-check the content type here for any case,
// although the new code does it automatically but it's good to have it here.
if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 {
if fname := ctx.Params().Get(paramName); fname != "" {
cType := TypeByFilename(fname)
ctx.ContentType(cType)
// re-check the content type here for any case,
// although the new code does it automatically but it's good to have it here.
if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists {
if fname := ctx.Params().Get(paramName); fname != "" {
cType := TypeByFilename(fname)
ctx.ContentType(cType)
}
}
}
}
requestPath = joinPath(fullpath, WildcardParam(paramName))
return rb.registerResourceRoute(requestPath, handler)
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
@ -378,12 +379,38 @@ var errNoOverlap = errors.New("invalid range: failed to overlap")
// The algorithm uses at most sniffLen bytes to make its decision.
const sniffLen = 512
func detectOrWriteContentType(ctx context.Context, name string, content io.ReadSeeker) (string, error) {
// If Content-Type isn't set, use the file's extension to find it, but
// if the Content-Type is unset explicitly, do not sniff the type.
ctypes, haveType := ctx.ResponseWriter().Header()["Content-Type"]
var ctype string
if !haveType {
ctype = TypeByExtension(filepath.Ext(name))
if ctype == "" {
// read a chunk to decide between utf-8 text and binary
var buf [sniffLen]byte
n, _ := io.ReadFull(content, buf[:])
ctype = http.DetectContentType(buf[:n])
_, err := content.Seek(0, io.SeekStart) // rewind to output whole file
if err != nil {
return "", err
}
}
ctx.ContentType(ctype)
} else if len(ctypes) > 0 {
ctype = ctypes[0]
}
return ctype, nil
}
// if name is empty, filename is unknown. (used for mime type, before sniffing)
// if modtime.IsZero(), modtime is unknown.
// content must be seeked to the beginning of the file.
// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker, gzip bool) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ {
func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ {
setLastModified(ctx, modtime)
done, rangeReq := checkPreconditions(ctx, modtime)
if done {
@ -394,27 +421,9 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc
// If Content-Type isn't set, use the file's extension to find it, but
// if the Content-Type is unset explicitly, do not sniff the type.
ctypes, haveType := ctx.ResponseWriter().Header()["Content-Type"]
var ctype string
if !haveType {
ctype = TypeByExtension(filepath.Ext(name))
if ctype == "" {
// read a chunk to decide between utf-8 text and binary
var buf [sniffLen]byte
n, _ := io.ReadFull(content, buf[:])
ctype = http.DetectContentType(buf[:n])
_, err := content.Seek(0, io.SeekStart) // rewind to output whole file
if err != nil {
return "seeker can't seek", http.StatusInternalServerError
}
}
ctx.ContentType(ctype)
} else if len(ctypes) > 0 {
ctype = ctypes[0]
ctype, err := detectOrWriteContentType(ctx, name, content)
if err != nil {
return "while seeking", http.StatusInternalServerError
}
size, err := sizeFunc()
@ -426,9 +435,6 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc
sendSize := size
var sendContent io.Reader = content
if gzip {
_ = ctx.GzipResponseWriter()
}
if size >= 0 {
ranges, err := parseRange(rangeReq, size)
if err != nil {
@ -496,7 +502,6 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc
}
ctx.Header("Accept-Ranges", "bytes")
if ctx.ResponseWriter().Header().Get(contentEncodingHeaderKey) == "" {
ctx.Header(contentLengthHeaderKey, strconv.FormatInt(sendSize, 10))
}
}
@ -827,12 +832,39 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo
}
ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()))
return dirList(ctx, f)
}
// serveContent will check modification time
sizeFunc := func() (int64, error) { return d.Size(), nil }
return serveContent(ctx, d.Name(), d.ModTime(), sizeFunc, f, gzip)
// if gzip disabled then continue using content byte ranges
if !gzip {
// serveContent will check modification time
sizeFunc := func() (int64, error) { return d.Size(), nil }
return serveContent(ctx, d.Name(), d.ModTime(), sizeFunc, f)
}
// else, set the last modified as "serveContent" does.
setLastModified(ctx, d.ModTime())
// write the file to the response writer.
contents, err := ioutil.ReadAll(f)
if err != nil {
ctx.Application().Logger().Debugf("err reading file: %v", err)
return "error reading the file", http.StatusInternalServerError
}
// Use `WriteNow` instead of `Write`
// because we need to know the compressed written size before
// the `FlushResponse`.
_, err = ctx.GzipResponseWriter().Write(contents)
if err != nil {
ctx.Application().Logger().Debugf("short write: %v", err)
return "short write", http.StatusInternalServerError
}
// try to find and send the correct content type based on the filename
// and the binary data inside "f".
detectOrWriteContentType(ctx, d.Name(), f)
return "", 200
}
// toHTTPError returns a non-specific HTTP error message and status code

View File

@ -303,6 +303,11 @@ var (
//
// A shortcut for the `context#LimitRequestBodySize`.
LimitRequestBodySize = context.LimitRequestBodySize
// Gzip is a middleware which enables writing
// using gzip compression, if client supports.
//
// A shortcut for the `context#Gzip`.
Gzip = context.Gzip
// FromStd converts native http.Handler, http.HandlerFunc & func(w, r, next) to context.Handler.
//
// Supported form types: