diff --git a/acceptencoder/acceptencoder.go b/acceptencoder/acceptencoder.go new file mode 100644 index 00000000..70fd6183 --- /dev/null +++ b/acceptencoder/acceptencoder.go @@ -0,0 +1,118 @@ +// Copyright 2015 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acceptencoder + +import ( + "compress/flate" + "compress/gzip" + "io" + "net/http" + "os" + "strconv" + "strings" + + "gopkg.in/bufio.v1" +) + +type q struct { + name string + value float64 +} + +// WriteFile reads from file and writes to writer by the specific encoding(gzip/deflate) + +func WriteFile(encoding string, writer io.Writer, file *os.File) (bool, string, error) { + return writeLevel(encoding, writer, file, flate.BestCompression) +} + +// WriteBody reads writes content to writer by the specific encoding(gzip/deflate) + +func WriteBody(encoding string, writer io.Writer, content []byte) (bool, string, error) { + return writeLevel(encoding, writer, bufio.NewBuffer(content), flate.BestSpeed) +} + +// writeLevel reads from reader,writes to writer by specific encoding and compress level +// the compress level is defined by deflate package + +func writeLevel(encoding string, writer io.Writer, reader io.Reader, level int) (bool, string, error) { + var outputWriter io.Writer + var err error + switch encoding { + case "gzip": + outputWriter, err = gzip.NewWriterLevel(writer, level) + case "deflate": + outputWriter, err = flate.NewWriter(writer, level) + default: + outputWriter = writer.(io.Writer) + } + if err != nil { + return false, "", err + } + if _, err = io.Copy(outputWriter, reader); err != nil { + return false, "", err + } + switch outputWriter.(type) { + case io.WriteCloser: + outputWriter.(io.WriteCloser).Close() + } + return encoding != "", encoding, nil +} + +// ParseEncoding will extract the right encoding for response +// the Accept-Encoding's sec is here: +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 + +func ParseEncoding(r *http.Request) string { + if r == nil { + return "" + } + return parseEncoding(r) +} + + +func parseEncoding(r *http.Request) string { + acceptEncoding := r.Header.Get("Accept-Encoding") + if acceptEncoding == "" { + return "" + } + var lastQ q + for _, v := range strings.Split(acceptEncoding, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + vs := strings.Split(v, ";") + if len(vs) == 1 { + lastQ = q{vs[0], 1} + break + } + if len(vs) == 2 { + f, _ := strconv.ParseFloat(strings.Replace(vs[1], "q=", "", -1), 64) + if f == 0 { + continue + } + if f > lastQ.value { + lastQ = q{vs[0], f} + } + } + } + if lastQ.name == "*" { + return "gzip" + } + if lastQ.name == "identity" { + return "" + } + return lastQ.name +} diff --git a/acceptencoder/acceptencoder_test.go b/acceptencoder/acceptencoder_test.go new file mode 100644 index 00000000..7b73b587 --- /dev/null +++ b/acceptencoder/acceptencoder_test.go @@ -0,0 +1,42 @@ +// Copyright 2015 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acceptencoder + +import "testing" + +func Test_ExtractEncoding(t *testing.T) { + if parseEncoding("gzip,deflate") != "gzip" { + t.Fail() + } + if parseEncoding("deflate,gzip") != "deflate" { + t.Fail() + } + + if parseEncoding("gzip;q=.5,deflate") != "deflate" { + t.Fail() + } + + if parseEncoding("gzip;q=0,deflate") != "deflate" { + t.Fail() + } + + if parseEncoding("deflate;q=0.5,gzip;q=0.5,identity") != "identity" { + t.Fail() + } + + if parseEncoding("*") != "gzip" { + t.Fail() + } +} diff --git a/context/output.go b/context/output.go index d132e392..a72c6f74 100644 --- a/context/output.go +++ b/context/output.go @@ -15,9 +15,8 @@ package context import ( + "beego/acceptencoder" "bytes" - "compress/flate" - "compress/gzip" "encoding/json" "encoding/xml" "errors" @@ -55,25 +54,12 @@ func (output *BeegoOutput) Header(key, val string) { // it sends out response body directly. func (output *BeegoOutput) Body(content []byte) { outputWriter := output.Context.ResponseWriter.(io.Writer) - if output.EnableGzip == true && output.Context.Input.Header("Accept-Encoding") != "" { - splitted := strings.SplitN(output.Context.Input.Header("Accept-Encoding"), ",", -1) - encodings := make([]string, len(splitted)) - - for i, val := range splitted { - encodings[i] = strings.TrimSpace(val) - } - for _, val := range encodings { - if val == "gzip" { - output.Header("Content-Encoding", "gzip") - outputWriter, _ = gzip.NewWriterLevel(output.Context.ResponseWriter, gzip.BestSpeed) - - break - } else if val == "deflate" { - output.Header("Content-Encoding", "deflate") - outputWriter, _ = flate.NewWriter(output.Context.ResponseWriter, flate.BestSpeed) - break - } - } + var encoding string + if output.EnableGzip { + encoding = acceptencoder.ParseEncoding(output.Context.Input.Request) + } + if b, n, _ := acceptencoder.WriteBody(encoding, outputWriter, content); b { + output.Header("Content-Encoding", n) } else { output.Header("Content-Length", strconv.Itoa(len(content))) } @@ -84,14 +70,6 @@ func (output *BeegoOutput) Body(content []byte) { output.Context.ResponseWriter.WriteHeader(output.Status) output.Status = 0 } - - outputWriter.Write(content) - switch outputWriter.(type) { - case *gzip.Writer: - outputWriter.(*gzip.Writer).Close() - case *flate.Writer: - outputWriter.(*flate.Writer).Close() - } } // Cookie sets cookie value via given key. diff --git a/memzipfile.go b/memzipfile.go index 376a1ae9..12b63eae 100644 --- a/memzipfile.go +++ b/memzipfile.go @@ -15,16 +15,12 @@ package beego import ( - "bufio" + "beego/acceptencoder" "bytes" - "compress/flate" - "compress/gzip" "errors" "io" "io/ioutil" - "net/http" "os" - "strings" "sync" "time" ) @@ -83,36 +79,17 @@ type memFileInfo struct { func newMenFileInfo(file *os.File, fileInfo os.FileInfo, zip string) (*memFileInfo, error) { var content []byte var zipBuf bytes.Buffer - var fileWriter io.Writer var err error - switch zip { - case "gzip": - fileWriter, err = gzip.NewWriterLevel(&zipBuf, gzip.BestCompression) - case "deflate": - fileWriter, err = flate.NewWriter(&zipBuf, flate.BestCompression) - default: - fileWriter = bufio.NewWriter(&zipBuf) - } + _, _, err = acceptencoder.WriteFile(zip, &zipBuf, file) if err != nil { return nil, err } - _, err = io.Copy(fileWriter, file) - if err != nil { - return nil, err - } - - switch fileWriter.(type) { - case io.WriteCloser: - fileWriter.(io.WriteCloser).Close() - } - content, err = ioutil.ReadAll(&zipBuf) if err != nil { return nil, err } - return &memFileInfo{ FileInfo: fileInfo, modTime: fileInfo.ModTime(), @@ -210,18 +187,3 @@ func (f *memFile) Seek(offset int64, whence int) (ret int64, err error) { f.offset = offset return f.offset, nil } - -// getAcceptEncodingZip returns accept encoding format in http header. -// zip is first, then deflate if both accepted. -// If no accepted, return empty string. -func getAcceptEncodingZip(r *http.Request) string { - ss := r.Header.Get("Accept-Encoding") - ss = strings.ToLower(ss) - if strings.Contains(ss, "gzip") { - return "gzip" - } else if strings.Contains(ss, "deflate") { - return "deflate" - } else { - return "" - } -} diff --git a/staticfile.go b/staticfile.go index de530b0c..20f7c519 100644 --- a/staticfile.go +++ b/staticfile.go @@ -15,6 +15,7 @@ package beego import ( + "beego/acceptencoder" "net/http" "os" "path" @@ -111,7 +112,7 @@ func serverStaticRouter(ctx *context.Context) { //to compress file var contentEncoding string if EnableGzip { - contentEncoding = getAcceptEncodingZip(ctx.Request) + contentEncoding = acceptencoder.ParseEncoding(ctx.Request) } memZipFile, err := openMemZipFile(filePath, contentEncoding) @@ -123,12 +124,10 @@ func serverStaticRouter(ctx *context.Context) { return } - if contentEncoding == "gzip" { - ctx.Output.Header("Content-Encoding", "gzip") - } else if contentEncoding == "deflate" { - ctx.Output.Header("Content-Encoding", "deflate") - } else { + if contentEncoding == "" { ctx.Output.Header("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) + } else { + ctx.Output.Header("Content-Encoding", contentEncoding) } http.ServeContent(ctx.ResponseWriter, ctx.Request, filePath, fileInfo.ModTime(), memZipFile)