diff --git a/examples/fileserver/fileserver.go b/examples/fileserver/fileserver.go index 1636d83..e91615b 100644 --- a/examples/fileserver/fileserver.go +++ b/examples/fileserver/fileserver.go @@ -9,14 +9,23 @@ import ( ) var ( - addr = flag.String("addr", ":8080", "TCP address to listen to") - dir = flag.String("dir", "/usr/share/nginx/html", "Directory to serve static files from") + addr = flag.String("addr", ":8080", "TCP address to listen to") + compress = flag.Bool("compress", false, "Enables transparent response compression if set to true") + dir = flag.String("dir", "/usr/share/nginx/html", "Directory to serve static files from") + generateIndexPages = flag.Bool("generateIndexPages", true, "Whether to generate directory index pages") ) func main() { flag.Parse() - h := fasthttp.FSHandler(*dir, 0) + fs := &fasthttp.FS{ + Root: *dir, + IndexNames: []string{"index.html"}, + GenerateIndexPages: *generateIndexPages, + Compress: *compress, + } + h := fs.NewRequestHandler() + if err := fasthttp.ListenAndServe(*addr, h); err != nil { log.Fatalf("error in ListenAndServe: %s", err) } diff --git a/fs.go b/fs.go index acafde9..1c89b6a 100644 --- a/fs.go +++ b/fs.go @@ -8,6 +8,7 @@ import ( "io" "mime" "os" + "path/filepath" "sort" "strings" "sync" @@ -65,12 +66,35 @@ 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. + // List of index file names to try opening during directory access. + // + // For example: + // + // * index.html + // * index.htm + // * my-super-index.xml + // + // By default the list is empty. + IndexNames []string + + // Index pages for directories without files matching IndexNames + // are automatically generated if set. // // By default index pages aren't generated. GenerateIndexPages bool + // Transparently compresses responses if set to true. + // + // The server tries minimizing CPU usage by caching compressed files. + // It adds FSCompressedFileSuffix suffix to the original file name and + // tries saving the resulting compressed file under the new file name. + // So it is advisable to give the server write access to Root + // and to all inner folders in order to minimze CPU usage when serving + // compressed responses. + // + // Transparent compression is disabled by default. + Compress bool + // Path rewriting function. // // By default request path is not modified. @@ -84,9 +108,13 @@ type FS struct { started bool } +// FS adds this suffix to the original file names when trying to store +// compressed file under the new file name. See FS.Compress for details. +const FSCompressedFileSuffix = ".fasthttp.gz" + // FSHandlerCacheDuration is the duration for caching open file handles // by FSHandler. -const FSHandlerCacheDuration = 5 * time.Second +const FSHandlerCacheDuration = 10 * time.Second // FSHandler returns request handler serving static files from // the given root folder. @@ -113,6 +141,7 @@ const FSHandlerCacheDuration = 5 * time.Second func FSHandler(root string, stripSlashes int) RequestHandler { fs := &FS{ Root: root, + IndexNames: []string{"index.html"}, GenerateIndexPages: true, PathRewrite: NewPathSlashesStripper(stripSlashes), } @@ -151,23 +180,22 @@ func (fs *FS) NewRequestHandler() RequestHandler { cacheDuration = FSHandlerCacheDuration } - pathRewrite := fs.PathRewrite - if pathRewrite == nil { - pathRewrite = NewPathSlashesStripper(0) - } - h := &fsHandler{ root: root, - pathRewrite: pathRewrite, + indexNames: fs.IndexNames, + pathRewrite: fs.PathRewrite, generateIndexPages: fs.GenerateIndexPages, + compress: fs.Compress, cacheDuration: cacheDuration, cache: make(map[string]*fsFile), + compressedCache: make(map[string]*fsFile), } go func() { + var pendingFiles []*fsFile for { time.Sleep(cacheDuration / 2) - h.cleanCache() + pendingFiles = h.cleanCache(pendingFiles) } }() @@ -176,13 +204,16 @@ func (fs *FS) NewRequestHandler() RequestHandler { type fsHandler struct { root string + indexNames []string pathRewrite PathRewriteFunc generateIndexPages bool + compress bool cacheDuration time.Duration - cache map[string]*fsFile - pendingFiles []*fsFile - cacheLock sync.Mutex + cache map[string]*fsFile + compressedCache map[string]*fsFile + pendingFiles []*fsFile + cacheLock sync.Mutex smallFileReaderPool sync.Pool } @@ -193,6 +224,7 @@ type fsFile struct { dirIndex []byte contentType string contentLength int + compressed bool lastModified time.Time lastModifiedStr []byte @@ -400,42 +432,60 @@ func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) { return r.offset, err } -func (h *fsHandler) cleanCache() { - t := time.Now() +func (h *fsHandler) cleanCache(pendingFiles []*fsFile) []*fsFile { + var filesToRelease []*fsFile + 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 { + // readers count on the previous run. + var remainingFiles []*fsFile + for _, ff := range pendingFiles { if ff.readersCount > 0 { - pendingFiles = append(pendingFiles, ff) + remainingFiles = append(remainingFiles, ff) } else { - ff.Release() + filesToRelease = append(filesToRelease, ff) } } - h.pendingFiles = pendingFiles + pendingFiles = remainingFiles - // Close stale file handles. - for k, ff := range h.cache { - if t.Sub(ff.t) > h.cacheDuration { + pendingFiles, filesToRelease = cleanCacheNolock(h.cache, pendingFiles, filesToRelease, h.cacheDuration) + pendingFiles, filesToRelease = cleanCacheNolock(h.compressedCache, pendingFiles, filesToRelease, h.cacheDuration) + + h.cacheLock.Unlock() + + for _, ff := range filesToRelease { + ff.Release() + } + + return pendingFiles +} + +func cleanCacheNolock(cache map[string]*fsFile, pendingFiles, filesToRelease []*fsFile, cacheDuration time.Duration) ([]*fsFile, []*fsFile) { + t := time.Now() + for k, ff := range cache { + if t.Sub(ff.t) > 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) + pendingFiles = append(pendingFiles, ff) } else { - ff.Release() + filesToRelease = append(filesToRelease, ff) } - delete(h.cache, k) + delete(cache, k) } } - - h.cacheLock.Unlock() + return pendingFiles, filesToRelease } func (h *fsHandler) handleRequest(ctx *RequestCtx) { - path := h.pathRewrite(ctx) + var path []byte + if h.pathRewrite != nil { + path = h.pathRewrite(ctx) + } else { + path = ctx.Path() + } path = stripTrailingSlashes(path) if n := bytes.IndexByte(path, 0); n >= 0 { @@ -444,8 +494,15 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) { return } + mustCompress := false + fileCache := h.cache + if h.compress && ctx.Request.Header.HasAcceptEncodingBytes(strGzip) { + mustCompress = true + fileCache = h.compressedCache + } + h.cacheLock.Lock() - ff, ok := h.cache[string(path)] + ff, ok := fileCache[string(path)] if ok { ff.readersCount++ } @@ -455,17 +512,12 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) { pathStr := string(path) filePath := h.root + pathStr var err error - ff, err = h.openFSFile(filePath) + ff, err = h.openFSFile(filePath, mustCompress) 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) + ff, err = h.openIndexFile(ctx, filePath, mustCompress) if err != nil { - ctx.Logger().Printf("Cannot create index for directory %q: %s", filePath, err) - ctx.Error("Cannot create directory index", StatusNotFound) + ctx.Logger().Printf("cannot open dir index %q: %s", filePath, err) + ctx.Error("Directory index is forbidden", StatusForbidden) return } } else if err != nil { @@ -475,9 +527,9 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) { } h.cacheLock.Lock() - ff1, ok := h.cache[pathStr] + ff1, ok := fileCache[pathStr] if !ok { - h.cache[pathStr] = ff + fileCache[pathStr] = ff ff.readersCount++ } else { ff1.readersCount++ @@ -506,14 +558,37 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) { return } + if ff.compressed { + ctx.Response.Header.SetCanonical(strContentEncoding, strGzip) + } ctx.Response.Header.SetCanonical(strLastModified, ff.lastModifiedStr) ctx.SetBodyStream(r, ff.contentLength) ctx.SetContentType(ff.contentType) + ctx.SetStatusCode(StatusOK) +} + +func (h *fsHandler) openIndexFile(ctx *RequestCtx, dirPath string, mustCompress bool) (*fsFile, error) { + for _, indexName := range h.indexNames { + indexFilePath := dirPath + "/" + indexName + ff, err := h.openFSFile(indexFilePath, mustCompress) + if err == nil { + return ff, nil + } + if !os.IsNotExist(err) { + return nil, fmt.Errorf("cannot open file %q: %s", indexFilePath, err) + } + } + + if !h.generateIndexPages { + return nil, fmt.Errorf("cannot access directory without index page. Directory %q", dirPath) + } + + return h.createDirIndex(ctx.URI(), dirPath, mustCompress) } var errDirIndexRequired = errors.New("directory index required") -func (h *fsHandler) createDirIndex(base *URI, filePath string) (*fsFile, error) { +func (h *fsHandler) createDirIndex(base *URI, dirPath string, mustCompress bool) (*fsFile, error) { var buf bytes.Buffer w := &buf @@ -530,7 +605,7 @@ func (h *fsHandler) createDirIndex(base *URI, filePath string) (*fsFile, error) fmt.Fprintf(w, `