185 lines
4.8 KiB
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")
|
|
}
|