diff --git a/config.go b/config.go index 3a50ea67..8c4f9f7d 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ var ( AppConfigPath string StaticDir map[string]string TemplateCache map[string]*template.Template + StaticExtensionsToGzip []string //Files which should also be compressed with gzip (.js, .css, etc) HttpAddr string HttpPort int HttpTLS bool @@ -68,6 +69,8 @@ func init() { StaticDir = make(map[string]string) StaticDir["/static"] = "static" + + StaticExtensionsToGzip = []string{".css", ".js"} TemplateCache = make(map[string]*template.Template) @@ -273,6 +276,23 @@ func ParseConfig() (err error) { } } } + + if sgz := AppConfig.String("StaticExtensionsToGzip"); sgz != "" { + extensions := strings.Split(sgz, ",") + if len(extensions) > 0 { + StaticExtensionsToGzip = []string{} + for _, ext := range extensions { + if len(ext) == 0 { + continue + } + extWithDot := ext + if extWithDot[:1] != "." { + extWithDot = "." + extWithDot + } + StaticExtensionsToGzip = append(StaticExtensionsToGzip, extWithDot) + } + } + } if enableadmin, err := AppConfig.Bool("EnableAdmin"); err == nil { EnableAdmin = enableadmin diff --git a/memzipfile.go b/memzipfile.go new file mode 100644 index 00000000..593c876b --- /dev/null +++ b/memzipfile.go @@ -0,0 +1,202 @@ +package beego + +import ( + "bytes" + "compress/flate" + "compress/gzip" + "errors" + //"fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + "time" +) + +var gmfim map[string]*MemFileInfo = make(map[string]*MemFileInfo) + +//TODO: 加锁保证数据完整性 +func OpenMemZipFile(path string, zip string) (*MemFile, error) { + osfile, e := os.Open(path) + if e != nil { + return nil, e + } + defer osfile.Close() + + osfileinfo, e := osfile.Stat() + if e != nil { + return nil, e + } + + modtime := osfileinfo.ModTime() + fileSize := osfileinfo.Size() + + cfi, ok := gmfim[zip+":"+path] + if ok && cfi.ModTime() == modtime && cfi.fileSize == fileSize { + //fmt.Printf("read %s file %s from cache\n", zip, path) + } else { + //fmt.Printf("NOT read %s file %s from cache\n", zip, path) + var content []byte + if zip == "gzip" { + //将文件内容压缩到zipbuf中 + var zipbuf bytes.Buffer + gzipwriter, e := gzip.NewWriterLevel(&zipbuf, gzip.BestCompression) + if e != nil { + return nil, e + } + _, e = io.Copy(gzipwriter, osfile) + gzipwriter.Close() + if e != nil { + return nil, e + } + //读zipbuf到content + content, e = ioutil.ReadAll(&zipbuf) + if e != nil { + return nil, e + } + } else if zip == "deflate" { + //将文件内容压缩到zipbuf中 + var zipbuf bytes.Buffer + deflatewriter, e := flate.NewWriter(&zipbuf, flate.BestCompression) + if e != nil { + return nil, e + } + _, e = io.Copy(deflatewriter, osfile) + deflatewriter.Close() + if e != nil { + return nil, e + } + //将zipbuf读入到content + content, e = ioutil.ReadAll(&zipbuf) + if e != nil { + return nil, e + } + } else { + content, e = ioutil.ReadAll(osfile) + if e != nil { + return nil, e + } + } + + cfi = &MemFileInfo{osfileinfo, modtime, content, int64(len(content)), fileSize} + gmfim[zip+":"+path] = cfi + //fmt.Printf("%s file %s to %d, cache it\n", zip, path, len(content)) + } + return &MemFile{fi: cfi, offset: 0}, nil +} + +type MemFileInfo struct { + os.FileInfo + modTime time.Time + content []byte + contentSize int64 + fileSize int64 +} + +func (fi *MemFileInfo) Name() string { + return fi.Name() +} + +func (fi *MemFileInfo) Size() int64 { + return fi.contentSize +} + +func (fi *MemFileInfo) Mode() os.FileMode { + return fi.Mode() +} + +func (fi *MemFileInfo) ModTime() time.Time { + return fi.modTime +} + +func (fi *MemFileInfo) IsDir() bool { + return fi.IsDir() +} + +func (fi *MemFileInfo) Sys() interface{} { + return nil +} + +type MemFile struct { + fi *MemFileInfo + offset int64 +} + +func (f *MemFile) Close() error { + return nil +} + +func (f *MemFile) Stat() (os.FileInfo, error) { + return f.fi, nil +} + +func (f *MemFile) Readdir(count int) ([]os.FileInfo, error) { + infos := []os.FileInfo{} + + return infos, nil +} + +func (f *MemFile) Read(p []byte) (n int, err error) { + if len(f.fi.content)-int(f.offset) >= len(p) { + n = len(p) + } else { + n = len(f.fi.content) - int(f.offset) + err = io.EOF + } + copy(p, f.fi.content[f.offset:f.offset+int64(n)]) + f.offset += int64(n) + return +} + +var errWhence = errors.New("Seek: invalid whence") +var errOffset = errors.New("Seek: invalid offset") + +func (f *MemFile) Seek(offset int64, whence int) (ret int64, err error) { + switch whence { + default: + return 0, errWhence + case os.SEEK_SET: + case os.SEEK_CUR: + offset += f.offset + case os.SEEK_END: + offset += int64(len(f.fi.content)) + } + if offset < 0 || int(offset) > len(f.fi.content) { + return 0, errOffset + } + f.offset = offset + return f.offset, nil +} + +//返回: gzip, deflate, 优先gzip +//返回空, 表示不zip +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 "" + } +} + +func CloseZWriter(zwriter io.Writer) { + if zwriter == nil { + return + } + + switch zwriter.(type) { + case *gzip.Writer: + zwriter.(*gzip.Writer).Close() + case *flate.Writer: + zwriter.(*flate.Writer).Close() + //其他情况不close, 保持和默认(非压缩)行为一致 + /* + case io.WriteCloser: + zwriter.(io.WriteCloser).Close() + */ + } +} diff --git a/router.go b/router.go index 018bb0ad..b40e1511 100644 --- a/router.go +++ b/router.go @@ -473,7 +473,39 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) middleware.Exception("403", rw, r, "403 Forbidden") goto Admin } - http.ServeFile(w, r, file) + + //This block obtained from (https://github.com/smithfox/beego) - it should probably get merged into astaxie/beego after a pull request + isStaticFileToCompress := false + if StaticExtensionsToGzip != nil && len(StaticExtensionsToGzip) > 0 { + for _, statExtension := range StaticExtensionsToGzip { + if strings.HasSuffix(strings.ToLower(file), strings.ToLower(statExtension)) { + isStaticFileToCompress = true + break + } + } + } + + if isStaticFileToCompress { + if EnableGzip { + w.contentEncoding = GetAcceptEncodingZip(r) + } + + memzipfile, err := OpenMemZipFile(file, w.contentEncoding) + if err != nil { + return + } + + w.InitHeadContent(finfo.Size()) + + if strings.HasSuffix(file, ".mustache") { + w.Header().Set("Content-Type", "text/html; charset=utf-8") //FIXME: hardcode + } + + http.ServeContent(w, r, file, finfo.ModTime(), memzipfile) + } else { + http.ServeFile(w, r, file) + } + w.started = true goto Admin } @@ -901,6 +933,7 @@ type responseWriter struct { writer http.ResponseWriter started bool status int + contentEncoding string } // Header returns the header map that will be sent by WriteHeader. @@ -908,6 +941,16 @@ func (w *responseWriter) Header() http.Header { return w.writer.Header() } +func (w *responseWriter) InitHeadContent(contentlength int64) { + if w.contentEncoding == "gzip" { + w.Header().Set("Content-Encoding", "gzip") + } else if w.contentEncoding == "deflate" { + w.Header().Set("Content-Encoding", "deflate") + } else { + w.Header().Set("Content-Length", strconv.FormatInt(contentlength, 10)) + } +} + // Write writes the data to the connection as part of an HTTP reply, // and sets `started` to true func (w *responseWriter) Write(p []byte) (int, error) {