github.com/astaxie/beego@v1.12.3/staticfile.go (about) 1 // Copyright 2014 beego Author. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package beego 16 17 import ( 18 "bytes" 19 "errors" 20 "net/http" 21 "os" 22 "path" 23 "path/filepath" 24 "strconv" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/astaxie/beego/context" 30 "github.com/astaxie/beego/logs" 31 "github.com/hashicorp/golang-lru" 32 ) 33 34 var errNotStaticRequest = errors.New("request not a static file request") 35 36 func serverStaticRouter(ctx *context.Context) { 37 if ctx.Input.Method() != "GET" && ctx.Input.Method() != "HEAD" { 38 return 39 } 40 41 forbidden, filePath, fileInfo, err := lookupFile(ctx) 42 if err == errNotStaticRequest { 43 return 44 } 45 46 if forbidden { 47 exception("403", ctx) 48 return 49 } 50 51 if filePath == "" || fileInfo == nil { 52 if BConfig.RunMode == DEV { 53 logs.Warn("Can't find/open the file:", filePath, err) 54 } 55 http.NotFound(ctx.ResponseWriter, ctx.Request) 56 return 57 } 58 if fileInfo.IsDir() { 59 requestURL := ctx.Input.URL() 60 if requestURL[len(requestURL)-1] != '/' { 61 redirectURL := requestURL + "/" 62 if ctx.Request.URL.RawQuery != "" { 63 redirectURL = redirectURL + "?" + ctx.Request.URL.RawQuery 64 } 65 ctx.Redirect(302, redirectURL) 66 } else { 67 //serveFile will list dir 68 http.ServeFile(ctx.ResponseWriter, ctx.Request, filePath) 69 } 70 return 71 } else if fileInfo.Size() > int64(BConfig.WebConfig.StaticCacheFileSize) { 72 //over size file serve with http module 73 http.ServeFile(ctx.ResponseWriter, ctx.Request, filePath) 74 return 75 } 76 77 var enableCompress = BConfig.EnableGzip && isStaticCompress(filePath) 78 var acceptEncoding string 79 if enableCompress { 80 acceptEncoding = context.ParseEncoding(ctx.Request) 81 } 82 b, n, sch, reader, err := openFile(filePath, fileInfo, acceptEncoding) 83 if err != nil { 84 if BConfig.RunMode == DEV { 85 logs.Warn("Can't compress the file:", filePath, err) 86 } 87 http.NotFound(ctx.ResponseWriter, ctx.Request) 88 return 89 } 90 91 if b { 92 ctx.Output.Header("Content-Encoding", n) 93 } else { 94 ctx.Output.Header("Content-Length", strconv.FormatInt(sch.size, 10)) 95 } 96 97 http.ServeContent(ctx.ResponseWriter, ctx.Request, filePath, sch.modTime, reader) 98 } 99 100 type serveContentHolder struct { 101 data []byte 102 modTime time.Time 103 size int64 104 originSize int64 //original file size:to judge file changed 105 encoding string 106 } 107 108 type serveContentReader struct { 109 *bytes.Reader 110 } 111 112 var ( 113 staticFileLruCache *lru.Cache 114 lruLock sync.RWMutex 115 ) 116 117 func openFile(filePath string, fi os.FileInfo, acceptEncoding string) (bool, string, *serveContentHolder, *serveContentReader, error) { 118 if staticFileLruCache == nil { 119 //avoid lru cache error 120 if BConfig.WebConfig.StaticCacheFileNum >= 1 { 121 staticFileLruCache, _ = lru.New(BConfig.WebConfig.StaticCacheFileNum) 122 } else { 123 staticFileLruCache, _ = lru.New(1) 124 } 125 } 126 mapKey := acceptEncoding + ":" + filePath 127 lruLock.RLock() 128 var mapFile *serveContentHolder 129 if cacheItem, ok := staticFileLruCache.Get(mapKey); ok { 130 mapFile = cacheItem.(*serveContentHolder) 131 } 132 lruLock.RUnlock() 133 if isOk(mapFile, fi) { 134 reader := &serveContentReader{Reader: bytes.NewReader(mapFile.data)} 135 return mapFile.encoding != "", mapFile.encoding, mapFile, reader, nil 136 } 137 lruLock.Lock() 138 defer lruLock.Unlock() 139 if cacheItem, ok := staticFileLruCache.Get(mapKey); ok { 140 mapFile = cacheItem.(*serveContentHolder) 141 } 142 if !isOk(mapFile, fi) { 143 file, err := os.Open(filePath) 144 if err != nil { 145 return false, "", nil, nil, err 146 } 147 defer file.Close() 148 var bufferWriter bytes.Buffer 149 _, n, err := context.WriteFile(acceptEncoding, &bufferWriter, file) 150 if err != nil { 151 return false, "", nil, nil, err 152 } 153 mapFile = &serveContentHolder{data: bufferWriter.Bytes(), modTime: fi.ModTime(), size: int64(bufferWriter.Len()), originSize: fi.Size(), encoding: n} 154 if isOk(mapFile, fi) { 155 staticFileLruCache.Add(mapKey, mapFile) 156 } 157 } 158 159 reader := &serveContentReader{Reader: bytes.NewReader(mapFile.data)} 160 return mapFile.encoding != "", mapFile.encoding, mapFile, reader, nil 161 } 162 163 func isOk(s *serveContentHolder, fi os.FileInfo) bool { 164 if s == nil { 165 return false 166 } else if s.size > int64(BConfig.WebConfig.StaticCacheFileSize) { 167 return false 168 } 169 return s.modTime == fi.ModTime() && s.originSize == fi.Size() 170 } 171 172 // isStaticCompress detect static files 173 func isStaticCompress(filePath string) bool { 174 for _, statExtension := range BConfig.WebConfig.StaticExtensionsToGzip { 175 if strings.HasSuffix(strings.ToLower(filePath), strings.ToLower(statExtension)) { 176 return true 177 } 178 } 179 return false 180 } 181 182 // searchFile search the file by url path 183 // if none the static file prefix matches ,return notStaticRequestErr 184 func searchFile(ctx *context.Context) (string, os.FileInfo, error) { 185 requestPath := filepath.ToSlash(filepath.Clean(ctx.Request.URL.Path)) 186 // special processing : favicon.ico/robots.txt can be in any static dir 187 if requestPath == "/favicon.ico" || requestPath == "/robots.txt" { 188 file := path.Join(".", requestPath) 189 if fi, _ := os.Stat(file); fi != nil { 190 return file, fi, nil 191 } 192 for _, staticDir := range BConfig.WebConfig.StaticDir { 193 filePath := path.Join(staticDir, requestPath) 194 if fi, _ := os.Stat(filePath); fi != nil { 195 return filePath, fi, nil 196 } 197 } 198 return "", nil, errNotStaticRequest 199 } 200 201 for prefix, staticDir := range BConfig.WebConfig.StaticDir { 202 if !strings.Contains(requestPath, prefix) { 203 continue 204 } 205 if prefix != "/" && len(requestPath) > len(prefix) && requestPath[len(prefix)] != '/' { 206 continue 207 } 208 filePath := path.Join(staticDir, requestPath[len(prefix):]) 209 if fi, err := os.Stat(filePath); fi != nil { 210 return filePath, fi, err 211 } 212 } 213 return "", nil, errNotStaticRequest 214 } 215 216 // lookupFile find the file to serve 217 // if the file is dir ,search the index.html as default file( MUST NOT A DIR also) 218 // if the index.html not exist or is a dir, give a forbidden response depending on DirectoryIndex 219 func lookupFile(ctx *context.Context) (bool, string, os.FileInfo, error) { 220 fp, fi, err := searchFile(ctx) 221 if fp == "" || fi == nil { 222 return false, "", nil, err 223 } 224 if !fi.IsDir() { 225 return false, fp, fi, err 226 } 227 if requestURL := ctx.Input.URL(); requestURL[len(requestURL)-1] == '/' { 228 ifp := filepath.Join(fp, "index.html") 229 if ifi, _ := os.Stat(ifp); ifi != nil && ifi.Mode().IsRegular() { 230 return false, ifp, ifi, err 231 } 232 } 233 return !BConfig.WebConfig.DirectoryIndex, fp, fi, err 234 }