Files
fasthttp/fs.go
T
2015-12-10 08:41:08 +02:00

674 lines
15 KiB
Go

package fasthttp
import (
"bytes"
"errors"
"fmt"
"html"
"io"
"mime"
"os"
"sort"
"strings"
"sync"
"time"
)
// PathRewriteFunc must return new request path based on arbitrary ctx
// info such as ctx.Path().
//
// Path rewriter is used in FS for translating the current request
// to the local filesystem path relative to FS.Root.
//
// The returned path may refer to ctx members. For example, ctx.Path().
type PathRewriteFunc func(ctx *RequestCtx) []byte
// NewPathSlashesStripper returns path rewriter, which strips slashesCount
// leading slashes from the path.
//
// Examples:
//
// * slashesCount = 0, original path: "/foo/bar", result: "/foo/bar"
// * slashesCount = 1, original path: "/foo/bar", result: "/bar"
// * slashesCount = 2, original path: "/foo/bar", result: ""
//
// The returned path rewriter may be used as FS.PathRewrite .
func NewPathSlashesStripper(slashesCount int) PathRewriteFunc {
return func(ctx *RequestCtx) []byte {
return stripLeadingSlashes(ctx.Path(), slashesCount)
}
}
// NewPathPrefixStripper returns path rewriter, which removes prefixSize bytes
// from the path prefix.
//
// Examples:
//
// * prefixSize = 0, original path: "/foo/bar", result: "/foo/bar"
// * prefixSize = 3, original path: "/foo/bar", result: "o/bar"
// * prefixSize = 7, original path: "/foo/bar", result: "r"
//
// The returned path rewriter may be used as FS.PathRewrite .
func NewPathPrefixStripper(prefixSize int) PathRewriteFunc {
return func(ctx *RequestCtx) []byte {
path := ctx.Path()
if len(path) >= prefixSize {
path = path[prefixSize:]
}
return path
}
}
// FS represents settings for request handler serving static files
// from the local filesystem.
type FS struct {
// Path to the root directory to serve files from.
Root string
// Index pages for directories without index.html are automatically
// generated if set.
//
// By default index pages aren't generated.
GenerateIndexPages bool
// Path rewriting function.
//
// By default request path is not modified.
PathRewrite PathRewriteFunc
// The duration for files' caching.
//
// FSHandlerCacheDuration is used by default.
CacheDuration time.Duration
started bool
}
// FSHandlerCacheDuration is the duration for caching open file handles
// by FSHandler.
const FSHandlerCacheDuration = 5 * time.Second
// FSHandler returns request handler serving static files from
// the given root folder.
//
// stripSlashes indicates how many leading slashes must be stripped
// from requested path before searching requested file in the root folder.
// Examples:
//
// * stripSlashes = 0, original path: "/foo/bar", result: "/foo/bar"
// * stripSlashes = 1, original path: "/foo/bar", result: "/bar"
// * stripSlashes = 2, original path: "/foo/bar", result: ""
//
// The returned request handler automatically generates index pages
// for directories without index.html.
//
// The returned handler caches requested file handles
// for FSHandlerCacheDuration.
// Make sure your program has enough 'max open files' limit aka
// 'ulimit -n' if root folder contains many files.
//
// Do not create multiple request handler instances for the same
// (root, stripSlashes) arguments - just reuse a single instance.
// Otherwise goroutine leak will occur.
func FSHandler(root string, stripSlashes int) RequestHandler {
fs := &FS{
Root: root,
GenerateIndexPages: true,
PathRewrite: NewPathSlashesStripper(stripSlashes),
}
return fs.NewRequestHandler()
}
// NewRequestHandler returns new request handler with the given FS settings.
//
// The returned handler caches requested file handles
// for FS.CacheDuration.
// Make sure your program has enough 'max open files' limit aka
// 'ulimit -n' if FS.Root folder contains many files.
//
// Do not create multiple request handlers from a single FS instance -
// just reuse a single request handler.
func (fs *FS) NewRequestHandler() RequestHandler {
if fs.started {
panic("BUG: NewRequestHandler() cannot be called multiple times for the same FS instance")
}
fs.started = true
root := fs.Root
// strip trailing slashes from the root path
for len(root) > 0 && root[len(root)-1] == '/' {
root = root[:len(root)-1]
}
// serve files from the current working directory if root is empty
if len(root) == 0 {
root = "."
}
cacheDuration := fs.CacheDuration
if cacheDuration <= 0 {
cacheDuration = FSHandlerCacheDuration
}
pathRewrite := fs.PathRewrite
if pathRewrite == nil {
pathRewrite = NewPathSlashesStripper(0)
}
h := &fsHandler{
root: root,
pathRewrite: pathRewrite,
generateIndexPages: fs.GenerateIndexPages,
cacheDuration: cacheDuration,
cache: make(map[string]*fsFile),
}
go func() {
for {
time.Sleep(cacheDuration / 2)
h.cleanCache()
}
}()
return h.handleRequest
}
type fsHandler struct {
root string
pathRewrite PathRewriteFunc
generateIndexPages bool
cacheDuration time.Duration
cache map[string]*fsFile
pendingFiles []*fsFile
cacheLock sync.Mutex
smallFileReaderPool sync.Pool
}
type fsFile struct {
h *fsHandler
f *os.File
dirIndex []byte
contentType string
contentLength int
lastModified time.Time
lastModifiedStr []byte
t time.Time
readersCount int
bigFiles []*bigFileReader
bigFilesLock sync.Mutex
}
func (ff *fsFile) NewReader() (io.Reader, error) {
if ff.isBig() {
r, err := ff.bigFileReader()
if err != nil {
ff.decReadersCount()
}
return r, err
}
return ff.smallFileReader(), nil
}
func (ff *fsFile) smallFileReader() io.Reader {
v := ff.h.smallFileReaderPool.Get()
if v == nil {
r := &fsSmallFileReader{
ff: ff,
}
r.v = r
return r
}
r := v.(*fsSmallFileReader)
r.ff = ff
if r.offset > 0 {
panic("BUG: fsSmallFileReader with non-nil offset found in the pool")
}
return r
}
// files bigger than this size are sent with sendfile
const maxSmallFileSize = 2 * 4096
func (ff *fsFile) isBig() bool {
return ff.contentLength > maxSmallFileSize && len(ff.dirIndex) == 0
}
func (ff *fsFile) bigFileReader() (io.Reader, error) {
if ff.f == nil {
panic("BUG: ff.f must be non-nil in bigFileReader")
}
var r io.Reader
ff.bigFilesLock.Lock()
n := len(ff.bigFiles)
if n > 0 {
r = ff.bigFiles[n-1]
ff.bigFiles = ff.bigFiles[:n-1]
}
ff.bigFilesLock.Unlock()
if r != nil {
return r, nil
}
f, err := os.Open(ff.f.Name())
if err != nil {
return nil, fmt.Errorf("cannot open already opened file: %s", err)
}
return &bigFileReader{
f: f,
ff: ff,
}, nil
}
func (ff *fsFile) Release() {
if ff.f != nil {
ff.f.Close()
if ff.isBig() {
ff.bigFilesLock.Lock()
for _, r := range ff.bigFiles {
r.f.Close()
}
ff.bigFilesLock.Unlock()
}
}
}
func (ff *fsFile) decReadersCount() {
ff.h.cacheLock.Lock()
ff.readersCount--
if ff.readersCount < 0 {
panic("BUG: negative fsFile.readersCount!")
}
ff.h.cacheLock.Unlock()
}
// bigFileReader attempts to trigger sendfile
// for sending big files over the wire.
type bigFileReader struct {
f *os.File
ff *fsFile
}
func (r *bigFileReader) Read(p []byte) (int, error) {
return r.f.Read(p)
}
func (r *bigFileReader) WriteTo(w io.Writer) (int64, error) {
if rf, ok := w.(io.ReaderFrom); ok {
// fast path. Senfile must be triggered
return rf.ReadFrom(r.f)
}
// slow path
return copyZeroAlloc(w, r.f)
}
func (r *bigFileReader) Close() error {
n, err := r.f.Seek(0, 0)
if err == nil {
if n != 0 {
panic("BUG: File.Seek(0,0) returned (non-zero, nil)")
}
ff := r.ff
ff.bigFilesLock.Lock()
ff.bigFiles = append(ff.bigFiles, r)
ff.bigFilesLock.Unlock()
} else {
r.f.Close()
}
r.ff.decReadersCount()
return err
}
type fsSmallFileReader struct {
ff *fsFile
offset int64
v interface{}
}
func (r *fsSmallFileReader) Close() error {
ff := r.ff
ff.decReadersCount()
r.ff = nil
r.offset = 0
ff.h.smallFileReaderPool.Put(r.v)
return nil
}
func (r *fsSmallFileReader) Read(p []byte) (int, error) {
ff := r.ff
if ff.f != nil {
n, err := ff.f.ReadAt(p, r.offset)
r.offset += int64(n)
return n, err
}
if r.offset == int64(len(ff.dirIndex)) {
return 0, io.EOF
}
n := copy(p, ff.dirIndex[r.offset:])
r.offset += int64(n)
return n, nil
}
func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) {
if r.offset != 0 {
panic("BUG: non-zero offset! Read() mustn't be called before WriteTo()")
}
ff := r.ff
var n int
var err error
if ff.f != nil {
if rf, ok := w.(io.ReaderFrom); ok {
return rf.ReadFrom(r)
}
bufv := copyBufPool.Get()
buf := bufv.([]byte)
for err != nil {
n, err = ff.f.ReadAt(buf, r.offset)
nw, errw := w.Write(buf[:n])
r.offset += int64(nw)
if errw == nil && nw != n {
panic("BUG: Write(p) returned (n, nil), where n != len(p)")
}
if err == nil {
err = errw
}
}
copyBufPool.Put(bufv)
if err == io.EOF {
err = nil
}
return r.offset, err
}
n, err = w.Write(ff.dirIndex)
r.offset += int64(n)
return r.offset, err
}
func (h *fsHandler) cleanCache() {
t := time.Now()
h.cacheLock.Lock()
// Close files which couldn't be closed before due to non-zero
// readers count.
var pendingFiles []*fsFile
for _, ff := range h.pendingFiles {
if ff.readersCount > 0 {
pendingFiles = append(pendingFiles, ff)
} else {
ff.Release()
}
}
h.pendingFiles = pendingFiles
// Close stale file handles.
for k, ff := range h.cache {
if t.Sub(ff.t) > h.cacheDuration {
if ff.readersCount > 0 {
// There are pending readers on stale file handle,
// so we cannot close it. Put it into pendingFiles
// so it will be closed later.
h.pendingFiles = append(h.pendingFiles, ff)
} else {
ff.Release()
}
delete(h.cache, k)
}
}
h.cacheLock.Unlock()
}
func (h *fsHandler) handleRequest(ctx *RequestCtx) {
path := h.pathRewrite(ctx)
path = stripTrailingSlashes(path)
if n := bytes.IndexByte(path, 0); n >= 0 {
ctx.Logger().Printf("cannot serve path with nil byte at position %d: %q", n, path)
ctx.Error("Are you a hacker?", StatusBadRequest)
return
}
h.cacheLock.Lock()
ff, ok := h.cache[string(path)]
if ok {
ff.readersCount++
}
h.cacheLock.Unlock()
if !ok {
pathStr := string(path)
filePath := h.root + pathStr
var err error
ff, err = h.openFSFile(filePath)
if err == errDirIndexRequired {
if !h.generateIndexPages {
ctx.Logger().Printf("An attempt to access directory without index page. Directory %q", filePath)
ctx.Error("Directory index is forbidden", StatusForbidden)
return
}
ff, err = h.createDirIndex(ctx.URI(), filePath)
if err != nil {
ctx.Logger().Printf("Cannot create index for directory %q: %s", filePath, err)
ctx.Error("Cannot create directory index", StatusNotFound)
return
}
} else if err != nil {
ctx.Logger().Printf("cannot open file %q: %s", filePath, err)
ctx.Error("Cannot open requested path", StatusNotFound)
return
}
h.cacheLock.Lock()
ff1, ok := h.cache[pathStr]
if !ok {
h.cache[pathStr] = ff
ff.readersCount++
} else {
ff1.readersCount++
}
h.cacheLock.Unlock()
if ok {
// The file has been already opened by another
// goroutine, so close the current file and use
// the file opened by another goroutine instead.
ff.Release()
ff = ff1
}
}
if !ctx.IfModifiedSince(ff.lastModified) {
ff.decReadersCount()
ctx.NotModified()
return
}
r, err := ff.NewReader()
if err != nil {
ctx.Logger().Printf("cannot obtain file reader for path=%q: %s", path, err)
ctx.Error("Internal Server Error", StatusInternalServerError)
return
}
ctx.Response.Header.SetCanonical(strLastModified, ff.lastModifiedStr)
ctx.SetBodyStream(r, ff.contentLength)
ctx.SetContentType(ff.contentType)
}
var errDirIndexRequired = errors.New("directory index required")
func (h *fsHandler) createDirIndex(base *URI, filePath string) (*fsFile, error) {
var buf bytes.Buffer
w := &buf
basePathEscaped := html.EscapeString(string(base.Path()))
fmt.Fprintf(w, "<html><head><title>%s</title></head><body>", basePathEscaped)
fmt.Fprintf(w, "<h1>%s</h1>", basePathEscaped)
fmt.Fprintf(w, "<ul>")
if len(basePathEscaped) > 1 {
fmt.Fprintf(w, `<li><a href="..">..</a></li>`)
}
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
fileinfos, err := f.Readdir(0)
f.Close()
if err != nil {
return nil, err
}
fm := make(map[string]os.FileInfo, len(fileinfos))
var filenames []string
for _, fi := range fileinfos {
name := fi.Name()
fm[name] = fi
filenames = append(filenames, name)
}
var u URI
base.CopyTo(&u)
u.Update(string(u.Path()) + "/")
sort.Sort(sort.StringSlice(filenames))
for _, name := range filenames {
u.Update(name)
pathEscaped := html.EscapeString(string(u.Path()))
fi := fm[name]
auxStr := "dir"
if !fi.IsDir() {
auxStr = fmt.Sprintf("file, %d bytes", fi.Size())
}
fmt.Fprintf(w, `<li><a href="%s">%s</a>, %s, last modified %s</li>`,
pathEscaped, html.EscapeString(name), auxStr, fi.ModTime())
}
fmt.Fprintf(w, "</ul></body></html>")
dirIndex := w.Bytes()
lastModified := time.Now()
ff := &fsFile{
h: h,
dirIndex: dirIndex,
contentType: "text/html",
contentLength: len(dirIndex),
lastModified: lastModified,
lastModifiedStr: AppendHTTPDate(nil, lastModified),
}
return ff, nil
}
func (h *fsHandler) openFSFile(filePath string) (*fsFile, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
fileStat, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
if fileStat.IsDir() {
f.Close()
indexPath := filePath + "/index.html"
ff, err := h.openFSFile(indexPath)
if err == nil {
return ff, nil
}
if !os.IsNotExist(err) {
return nil, err
}
return nil, errDirIndexRequired
}
n := fileStat.Size()
contentLength := int(n)
if n != int64(contentLength) {
f.Close()
return nil, fmt.Errorf("too big file: %d bytes", n)
}
ext := fileExtension(filePath)
contentType := mime.TypeByExtension(ext)
lastModified := fileStat.ModTime()
ff := &fsFile{
h: h,
f: f,
contentType: contentType,
contentLength: contentLength,
lastModified: lastModified,
lastModifiedStr: AppendHTTPDate(nil, lastModified),
}
return ff, nil
}
func stripLeadingSlashes(path []byte, stripSlashes int) []byte {
for stripSlashes > 0 && len(path) > 0 {
if path[0] != '/' {
panic("BUG: path must start with slash")
}
n := bytes.IndexByte(path[1:], '/')
if n < 0 {
path = path[:0]
break
}
path = path[n+1:]
stripSlashes--
}
return path
}
func stripTrailingSlashes(path []byte) []byte {
for len(path) > 0 && path[len(path)-1] == '/' {
path = path[:len(path)-1]
}
return path
}
func fileExtension(path string) string {
n := strings.LastIndexByte(path, '.')
if n < 0 {
return ""
}
return path[n:]
}
func fsLastModified(path string) (time.Time, error) {
f, err := os.Open(path)
if err != nil {
return zeroTime, err
}
fileInfo, err := f.Stat()
f.Close()
if err != nil {
return zeroTime, err
}
return fileInfo.ModTime().In(gmtLocation).Truncate(time.Second), nil
}