httprouter/fs.go

185 lines
4.8 KiB
Go

// Copyright 2021 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gohttprouter
import (
"embed"
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"os"
"path"
"strings"
)
// FS file system for serving static content.
type FS struct {
StaticFiles *embed.FS
FolderPrefix string
UseLocalFolder bool
LocalFolderPrefix string
HeaderHandler HeaderHandler
IndexFileNames []string
}
// NewFS creates a new instance of the http file system used for serving static files.
func NewFS(staticFiles *embed.FS) *FS {
return &FS{
StaticFiles: staticFiles,
HeaderHandler: DefaultHeaderHandler,
}
}
func (fs *FS) ServeHTTP(w http.ResponseWriter, r *http.Request, info RoutingInfo) {
// use wildcard placeholder value for the path (if available)
url, found := info.Parameters["*"]
if !found {
url = r.URL.Path
}
// search for file in static file system
file, err := fs.findFile(url)
if err != nil {
http.NotFound(w, r)
return
}
// check, if the file is a folder
file, ok := fs.checkFolder(file, url)
if !ok {
http.NotFound(w, r)
return
}
defer file.Close()
// serve file content
fs.serve(w, r, info, file)
}
func (fs *FS) findFile(url string) (fs.File, error) {
// build file path
filePath := path.Join(fs.FolderPrefix, url)
filePath = strings.TrimPrefix(filePath, "/")
// check, if file exists in local folder
if fs.UseLocalFolder {
localFilePath := path.Join(fs.LocalFolderPrefix, filePath)
localFilePath = strings.TrimPrefix(localFilePath, "/")
if localFilePath == "" {
localFilePath = "."
}
file, err := os.Open(localFilePath)
if err == nil {
return file, nil
}
}
// use static file system
if filePath == "" {
filePath = "."
}
file, err := fs.StaticFiles.Open(filePath)
return file, err
}
func (fs *FS) checkFolder(file fs.File, url string) (fs.File, bool) {
fileInfo, err := file.Stat()
if err != nil {
_ = file.Close()
return file, false
}
// check for folder / root location
if fileInfo.IsDir() {
// close folder handler (we don't need it anymore)
_ = file.Close()
// check for index files
for _, indexFileName := range fs.IndexFileNames {
newFullPath := path.Join(url, indexFileName)
if newFile, err := fs.findFile(newFullPath); err == nil {
return newFile, true
}
}
return file, false
}
return file, true
}
func (fs *FS) serve(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
// execute header handler
if fs.HeaderHandler != nil {
fs.HeaderHandler(w, r, info, file)
}
w.WriteHeader(http.StatusOK)
// check for head request
if r.Method == http.MethodHead {
// return no content
return
}
// send file content
io.Copy(w, file)
}
// HeaderHandler for custom http header handling
type HeaderHandler func(http.ResponseWriter, *http.Request, RoutingInfo, fs.File)
// DefaultHeaderHandler is the default implementation for file headers
func DefaultHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
ContentLengthHeaderHandler(w, r, info, file)
ContentTypeHeaderHandler(w, r, info, file)
CacheControlHeaderHandler(w, r, info, file)
ContentTypeOptionHeaderHandler(w, r, info, file)
}
// ContentLengthHeaderHandler sends the file size
func ContentLengthHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
// send file size as header
stat, err := file.Stat()
if err != nil {
return
}
w.Header().Set("Content-Length", fmt.Sprint(stat.Size()))
}
// ContentTypeHeaderHandler sends content type based on the file name extension
func ContentTypeHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
stat, err := file.Stat()
if err != nil {
return
}
mimeHeader := mime.TypeByExtension(path.Ext(stat.Name()))
if mimeHeader != "" {
w.Header().Set("Content-Type", mimeHeader)
}
}
// CacheControlHeaderHandler send cache control header of 7 days for the browser and 30 days for the CDN
func CacheControlHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, s-maxage=%d, max-age=%d", 60*60*24*30, 60*60*24*7))
}
// ContentTypeOptionHeaderHandler for nosniff content type option
func ContentTypeOptionHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
w.Header().Set("X-Content-Type-Options", "nosniff")
}