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  }