Skip to content

Commit 14cc6a8

Browse files
committed
add first draft
0 parents  commit 14cc6a8

9 files changed

Lines changed: 673 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.gobuild*

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/codemodify/simple-http-fileserver
2+
3+
go 1.26

license

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# The Free License
3+
# https://licenseplanet.net/the-free-license
4+
#
5+
# ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~
6+
# @Authors : Nicolae Carabut
7+
# @URLs : https://github.com/codemodify/simple-http-fileserver
8+
# @Contributors :
9+
# ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~
10+
#
11+
# You are free to use this art project. No restrictions.
12+
# No name dropping unless permitted.
13+
# My hope is simply that this will be used to empower people.
14+
#

main.go

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"net/http"
10+
"os"
11+
"os/signal"
12+
"syscall"
13+
"time"
14+
)
15+
16+
var version = "dev"
17+
18+
const (
19+
defaultHTTPOn = "127.0.0.1:64080"
20+
defaultHTTPSOn = "127.0.0.1:64443"
21+
)
22+
23+
func init() {
24+
log.SetFlags(0)
25+
}
26+
27+
func main() {
28+
workFolder := "."
29+
if cwd, err := os.Getwd(); err == nil {
30+
workFolder = cwd
31+
}
32+
33+
// flags
34+
showVersion := flag.Bool("version", false, "print version and exit")
35+
folder := flag.String("folder", workFolder, "folder")
36+
37+
enableHTTP := flag.Bool("enableHTTP", true, "enable HTTP")
38+
httpOn := flag.String("httpOn", defaultHTTPOn, "http On")
39+
40+
enableHTTPS := flag.Bool("enableHTTPS", false, "enable HTTPS")
41+
httpsOn := flag.String("httpsOn", defaultHTTPSOn, "https On")
42+
httpsCert := flag.String("httpsCert", "", "https Cert")
43+
httpsKey := flag.String("httpsKey", "", "https Key")
44+
45+
enableDirListing := flag.Bool("enableDirListing", false, "enable directory listing")
46+
enableFileNotFound := flag.Bool("enableFileNotFound", true, "enable 404 page")
47+
fileNotFoundFile := flag.String("fileNotFoundFile", "404.html", "fallback file")
48+
49+
flag.Parse()
50+
51+
if *showVersion {
52+
fmt.Println(version)
53+
return
54+
}
55+
56+
if err := validateServerConfig(*enableHTTP, *enableHTTPS, *httpsCert, *httpsKey); err != nil {
57+
logError("%v", err)
58+
}
59+
60+
handler := accessLog(newFileHandler(*folder, *enableDirListing, *enableFileNotFound, *fileNotFoundFile))
61+
62+
if *enableHTTP {
63+
logInfo("listen | http://%s", *httpOn)
64+
}
65+
if *enableHTTPS {
66+
logInfo("listen | https://%s", *httpsOn)
67+
}
68+
logInfo("folder | %s", *folder)
69+
70+
const (
71+
readHeaderTimeout = 10 * time.Second
72+
idleTimeout = 120 * time.Second
73+
shutdownTimeout = 5 * time.Second
74+
)
75+
76+
var servers []*http.Server
77+
errCh := make(chan error, 2)
78+
79+
if *enableHTTP {
80+
srv := &http.Server{
81+
Addr: *httpOn,
82+
Handler: handler,
83+
ReadHeaderTimeout: readHeaderTimeout,
84+
IdleTimeout: idleTimeout,
85+
}
86+
servers = append(servers, srv)
87+
go func() {
88+
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
89+
errCh <- fmt.Errorf("http server on %s: %w", srv.Addr, err)
90+
}
91+
}()
92+
}
93+
94+
if *enableHTTPS {
95+
srv := &http.Server{
96+
Addr: *httpsOn,
97+
Handler: handler,
98+
ReadHeaderTimeout: readHeaderTimeout,
99+
IdleTimeout: idleTimeout,
100+
}
101+
servers = append(servers, srv)
102+
go func() {
103+
if err := srv.ListenAndServeTLS(*httpsCert, *httpsKey); err != nil && !errors.Is(err, http.ErrServerClosed) {
104+
errCh <- fmt.Errorf("https server on %s: %w", srv.Addr, err)
105+
}
106+
}()
107+
}
108+
109+
sigCh := make(chan os.Signal, 1)
110+
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
111+
112+
var serverErr error
113+
select {
114+
case serverErr = <-errCh:
115+
logInfo("err | %v", serverErr)
116+
case sig := <-sigCh:
117+
logInfo("signal | %s", sig)
118+
}
119+
120+
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
121+
for _, srv := range servers {
122+
if err := srv.Shutdown(shutdownCtx); err != nil {
123+
logInfo("err | shutdown: %v", err)
124+
}
125+
}
126+
cancel()
127+
128+
if serverErr != nil {
129+
os.Exit(1)
130+
}
131+
}
132+
133+
func validateServerConfig(enableHTTP, enableHTTPS bool, httpsCert, httpsKey string) error {
134+
if !enableHTTP && !enableHTTPS {
135+
return errors.New("either '--enableHTTP' or '--enableHTTPS' is required")
136+
}
137+
138+
if enableHTTPS {
139+
if httpsCert == "" {
140+
return errors.New("'--httpsCert' is required when '--enableHTTPS' is set")
141+
}
142+
if httpsKey == "" {
143+
return errors.New("'--httpsKey' is required when '--enableHTTPS' is set")
144+
}
145+
}
146+
147+
return nil
148+
}
149+
150+
func newFileHandler(folder string, enableDirListing bool, enableFileNotFound bool, fileNotFoundFile string) http.Handler {
151+
var fileSystem http.FileSystem = http.Dir(folder)
152+
if !enableDirListing {
153+
fileSystem = noDirListingFileSystem{fileSystem}
154+
}
155+
fileServer := http.FileServer(fileSystem)
156+
157+
if !enableFileNotFound {
158+
return fileServer
159+
}
160+
161+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162+
rec := &notFoundRecorder{
163+
ResponseWriter: w,
164+
header: make(http.Header),
165+
}
166+
fileServer.ServeHTTP(rec, r)
167+
if rec.notFound {
168+
serveNotFoundFile(w, r, fileSystem, fileNotFoundFile)
169+
}
170+
})
171+
}
172+
173+
type noDirListingFileSystem struct {
174+
fs http.FileSystem
175+
}
176+
177+
func (ndfs noDirListingFileSystem) Open(name string) (http.File, error) {
178+
f, err := ndfs.fs.Open(name)
179+
if err != nil {
180+
return nil, err
181+
}
182+
info, err := f.Stat()
183+
if err != nil {
184+
f.Close()
185+
return nil, err
186+
}
187+
if info.IsDir() {
188+
idx, err := ndfs.fs.Open(name + "/index.html")
189+
if err != nil {
190+
f.Close()
191+
return nil, os.ErrNotExist
192+
}
193+
idx.Close()
194+
}
195+
return f, nil
196+
}
197+
198+
// notFoundRecorder wraps a ResponseWriter and intercepts 404 responses from
199+
// http.FileServer so the handler can substitute a custom not-found page.
200+
// It uses its own header map to prevent header leakage from the suppressed
201+
// response into the replacement response.
202+
type notFoundRecorder struct {
203+
http.ResponseWriter
204+
header http.Header
205+
notFound bool
206+
headerSent bool
207+
}
208+
209+
func (rec *notFoundRecorder) Header() http.Header {
210+
return rec.header
211+
}
212+
213+
func (rec *notFoundRecorder) WriteHeader(code int) {
214+
if rec.headerSent {
215+
return
216+
}
217+
rec.headerSent = true
218+
if code == http.StatusNotFound {
219+
rec.notFound = true
220+
return
221+
}
222+
dst := rec.ResponseWriter.Header()
223+
for k, vv := range rec.header {
224+
dst[k] = vv
225+
}
226+
rec.ResponseWriter.WriteHeader(code)
227+
}
228+
229+
func (rec *notFoundRecorder) Write(b []byte) (int, error) {
230+
if !rec.headerSent {
231+
rec.WriteHeader(http.StatusOK)
232+
}
233+
if rec.notFound {
234+
return len(b), nil
235+
}
236+
return rec.ResponseWriter.Write(b)
237+
}
238+
239+
func serveNotFoundFile(w http.ResponseWriter, r *http.Request, fileSystem http.FileSystem, name string) {
240+
file, err := fileSystem.Open(name)
241+
if err != nil {
242+
http.NotFound(w, r)
243+
return
244+
}
245+
defer file.Close()
246+
247+
fileInfo, err := file.Stat()
248+
if err != nil || fileInfo.IsDir() {
249+
http.NotFound(w, r)
250+
return
251+
}
252+
253+
http.ServeContent(&staticStatusResponseWriter{
254+
ResponseWriter: w,
255+
statusCode: http.StatusNotFound,
256+
}, r, fileInfo.Name(), fileInfo.ModTime(), file)
257+
}
258+
259+
type staticStatusResponseWriter struct {
260+
http.ResponseWriter
261+
statusCode int
262+
headerWritten bool
263+
}
264+
265+
func (w *staticStatusResponseWriter) WriteHeader(statusCode int) {
266+
if w.headerWritten {
267+
return
268+
}
269+
270+
w.headerWritten = true
271+
w.ResponseWriter.WriteHeader(w.statusCode)
272+
}
273+
274+
func (w *staticStatusResponseWriter) Write(p []byte) (int, error) {
275+
if !w.headerWritten {
276+
w.WriteHeader(http.StatusOK)
277+
}
278+
279+
return w.ResponseWriter.Write(p)
280+
}
281+
282+
func accessLog(next http.Handler) http.Handler {
283+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
284+
logInfo("%s %s start", r.Method, r.URL.Path)
285+
rec := &statusRecorder{ResponseWriter: w}
286+
start := time.Now()
287+
next.ServeHTTP(rec, r)
288+
logInfo("%s %s done - %d %s", r.Method, r.URL.Path, rec.status, time.Since(start).Round(time.Millisecond))
289+
})
290+
}
291+
292+
type statusRecorder struct {
293+
http.ResponseWriter
294+
status int
295+
}
296+
297+
func (rec *statusRecorder) WriteHeader(code int) {
298+
rec.status = code
299+
rec.ResponseWriter.WriteHeader(code)
300+
}
301+
302+
func (rec *statusRecorder) Write(b []byte) (int, error) {
303+
if rec.status == 0 {
304+
rec.status = http.StatusOK
305+
}
306+
return rec.ResponseWriter.Write(b)
307+
}
308+
309+
func logInfo(format string, v ...interface{}) {
310+
log.Default().Printf("I | %s | %s | %s \n", version, timeNowAsString(), fmt.Sprintf(format, v...))
311+
}
312+
313+
func logError(format string, v ...interface{}) {
314+
log.Default().Fatalf("E | %s | %s | %s \n", version, timeNowAsString(), fmt.Sprintf(format, v...))
315+
}
316+
317+
func timeNowAsString() string {
318+
return time.Now().UTC().Format(time.RFC3339)
319+
}

0 commit comments

Comments
 (0)