This repository has been archived by the owner on Mar 16, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 74
/
fileserver.go
141 lines (108 loc) · 3.93 KB
/
fileserver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Package fileserver contains static files server implementation.
package fileserver
import (
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
const (
defaultFallbackErrorContent = "<html><body><h1>Error {{ code }}</h1><h2>{{ message }}</h2></body></html>"
defaultIndexFileName = "index.html"
)
// ErrorHandlerFunc is used as handler for errors processing. If func return `true` - next handler will be NOT executed.
type ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, fs *FileServer, errorCode int) (doNotContinue bool)
// FileServer is a main file server structure (implements `http.Handler` interface).
type FileServer struct {
// Server settings (some of them can be changed in runtime).
Settings Settings
// If all error handlers fails - this content will be used as fallback for error page generating.
FallbackErrorContent string
// Error handlers stack.
ErrorHandlers []ErrorHandlerFunc
}
// Settings describes file server options.
type Settings struct {
// Directory path, where files for serving is located.
FilesRoot string
// File name (relative path to the file) that will be used as an index (like <https://bit.ly/356QeFm>).
IndexFileName string
// File name (relative path to the file) that will be used as error page template.
ErrorFileName string
// Respond "index file" request with redirection to the root (`example.com/index.html` -> `example.com/`).
RedirectIndexFileToRoot bool
}
// NewFileServer creates new file server with default settings. Feel free to change default behavior.
func NewFileServer(s Settings) (*FileServer, error) { //nolint:gocritic
if info, err := os.Stat(s.FilesRoot); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf(`directory "%s" does not exists`, s.FilesRoot)
}
return nil, err
} else if !info.IsDir() {
return nil, fmt.Errorf(`"%s" is not directory`, s.FilesRoot)
}
if s.IndexFileName == "" {
s.IndexFileName = defaultIndexFileName
}
fs := &FileServer{
Settings: s,
FallbackErrorContent: defaultFallbackErrorContent,
}
fs.ErrorHandlers = []ErrorHandlerFunc{
JSONErrorHandler(),
StaticHTMLPageErrorHandler(),
}
return fs, nil
}
func (fs *FileServer) handleError(w http.ResponseWriter, r *http.Request, errorCode int) {
if fs.ErrorHandlers != nil && len(fs.ErrorHandlers) > 0 {
for _, handler := range fs.ErrorHandlers {
if handler(w, r, fs, errorCode) {
return
}
}
}
// fallback
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(errorCode)
_, _ = w.Write([]byte(ErrorPageTemplate(fs.FallbackErrorContent).Build(errorCode)))
}
// ServeHTTP responds to an HTTP request.
func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.handleError(w, r, http.StatusMethodNotAllowed)
return
}
if fs.Settings.RedirectIndexFileToRoot && len(fs.Settings.IndexFileName) > 0 {
// redirect .../index.html to .../
if strings.HasSuffix(r.URL.Path, "/"+fs.Settings.IndexFileName) {
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len(fs.Settings.IndexFileName)], http.StatusMovedPermanently)
return
}
}
urlPath := r.URL.Path
// add leading `/` (if required)
if urlPath == "" || !strings.HasPrefix(urlPath, "/") {
urlPath = "/" + r.URL.Path
}
// if directory requested (or server root) - add index file name
if len(fs.Settings.IndexFileName) > 0 && urlPath[len(urlPath)-1] == '/' {
urlPath += fs.Settings.IndexFileName
}
// prepare target file path
filePath := path.Join(fs.Settings.FilesRoot, filepath.FromSlash(path.Clean(urlPath)))
// check for file existence
if stat, err := os.Stat(filePath); err == nil && stat.Mode().IsRegular() {
if file, err := os.Open(filePath); err == nil {
defer func() { _ = file.Close() }()
http.ServeContent(w, r, filepath.Base(filePath), stat.ModTime(), file)
return
}
fs.handleError(w, r, http.StatusInternalServerError)
return
}
fs.handleError(w, r, http.StatusNotFound)
}