forked from mitranim/srv
-
Notifications
You must be signed in to change notification settings - Fork 0
/
srv.go
139 lines (120 loc) · 3.23 KB
/
srv.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
/*
Extremely simple Go tool that serves files out of a given folder, using a file
resolution algorithm similar to GitHub Pages, Netlify, or the default Nginx
config. Useful for local development. Provides a Go "library" (less than 100
LoC) and an optional CLI tool.
See `readme.md` for examples and additional details.
*/
package srv
import (
"archive/zip"
"errors"
"io"
"io/fs"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
const (
ZIP_EXT = `.zip`
)
/*
Serves static files, resolving URL/HTML in a fashion similar to the default
Nginx config, Github Pages, and Netlify. Implements `http.Handler`. Can be used
as an almost drop-in replacement for `http.FileServer`.
*/
type FileServer string
/*
Implements `http.Hander`.
Minor note: this has a race condition between checking for a file's existence
and actually serving it. Serving a file is not an atomic operation; the file
may be deleted or changed midway. In a production-grade version, this condition
would probably be addressed.
*/
func (self FileServer) ServeHTTP(rew http.ResponseWriter, req *http.Request) {
switch req.Method {
default:
http.Error(rew, "", http.StatusMethodNotAllowed)
return
case http.MethodHead, http.MethodOptions:
return
case http.MethodGet:
}
dir := string(self)
reqPath := req.URL.Path
filePath := fpj(dir, reqPath)
zipFile, inZipFile := splitFilePathWithExt(filePath, ZIP_EXT)
/**
Ends with slash? Return error 404 for hygiene. Directory links must not end
with a slash. It's unnecessary, and GH Pages will do a 301 redirect to a
non-slash URL, which is a good feature but adds latency.
*/
// if len(reqPath) > 1 && reqPath[len(reqPath)-1] == '/' {
// goto notFound
// }
if fileExists(filePath) {
http.ServeFile(rew, req, filePath)
return
}
if fileExists(zipFile) {
zipReader, err := zip.OpenReader(zipFile)
if err != nil {
panic(err)
}
defer zipReader.Close()
req.URL.Path = inZipFile
file, err := zipReader.Open(inZipFile)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
goto notFound
}
panic(err)
}
rew.Header().Set(`Content-Type`, mime.TypeByExtension(filepath.Ext(inZipFile)))
io.Copy(rew, file)
return
}
// Has extension? Don't bother looking for +".html" or +"/index.html".
if path.Ext(reqPath) != "" {
goto notFound
}
// Try +".html".
{
candidatePath := filePath + ".html"
if fileExists(candidatePath) {
http.ServeFile(rew, req, candidatePath)
return
}
}
// Try +"/index.html".
{
candidatePath := fpj(filePath, "index.html")
if fileExists(candidatePath) {
http.ServeFile(rew, req, candidatePath)
return
}
}
notFound:
// Minor issue: sends code 200 instead of 404 if "404.html" is found; not
// worth fixing for local development.
http.ServeFile(rew, req, fpj(dir, "404.html"))
}
func fpj(path ...string) string { return filepath.Join(path...) }
func fileExists(filePath string) bool {
stat, _ := os.Stat(filePath)
return stat != nil && !stat.IsDir()
}
func splitFilePathWithExt(val string, ext string) (arch string, file string) {
vals := strings.Split(val, string(os.PathSeparator))
for ind, name := range vals {
if filepath.Ext(name) == ext {
arch = filepath.Join(vals[:ind+1]...)
file = filepath.Join(vals[ind+1:]...)
break
}
}
return
}