11package server
22
33import (
4+ "bytes"
5+ "crypto/md5"
6+ "encoding/hex"
47 "fmt"
58 "net/http"
69 "net/http/httputil"
@@ -15,6 +18,140 @@ import (
1518 "github.com/mirkobrombin/goup/internal/server/middleware"
1619)
1720
21+ // Memory cache for static files.
22+ var staticFileCache sync.Map
23+
24+ type cachedFile struct {
25+ content []byte
26+ modTime time.Time
27+ etag string
28+ }
29+
30+ var (
31+ sharedProxyMap = make (map [string ]* httputil.ReverseProxy )
32+ sharedProxyMapMu sync.Mutex
33+ defaultTransport = & http.Transport {}
34+
35+ globalBytePool = & byteSlicePool {
36+ pool : sync.Pool {
37+ New : func () interface {} {
38+ return make ([]byte , 32 * 1024 )
39+ },
40+ },
41+ }
42+ )
43+
44+ // getSharedReverseProxy returns a shared ReverseProxy for the given backend URL.
45+ func getSharedReverseProxy (rawURL string ) (* httputil.ReverseProxy , error ) {
46+ sharedProxyMapMu .Lock ()
47+ defer sharedProxyMapMu .Unlock ()
48+
49+ if rp , ok := sharedProxyMap [rawURL ]; ok {
50+ return rp , nil
51+ }
52+
53+ parsedURL , err := url .Parse (rawURL )
54+ if err != nil {
55+ return nil , err
56+ }
57+
58+ rp := httputil .NewSingleHostReverseProxy (parsedURL )
59+ rp .Transport = defaultTransport
60+ rp .BufferPool = globalBytePool
61+
62+ sharedProxyMap [rawURL ] = rp
63+ return rp , nil
64+ }
65+
66+ type byteSlicePool struct {
67+ pool sync.Pool
68+ }
69+
70+ func (b * byteSlicePool ) Get () []byte {
71+ return b .pool .Get ().([]byte )
72+ }
73+
74+ func (b * byteSlicePool ) Put (buf []byte ) {
75+ if cap (buf ) == 32 * 1024 {
76+ b .pool .Put (buf [:32 * 1024 ])
77+ }
78+ }
79+
80+ // computeETag returns an ETag string for the given data.
81+ func computeETag (data []byte ) string {
82+ sum := md5 .Sum (data )
83+ return `"` + hex .EncodeToString (sum [:]) + `"`
84+ }
85+
86+ // memoryCachedFileServer returns a handler that serves (and caches) static files from memory.
87+ func memoryCachedFileServer (dir string , headers map [string ]string ) http.Handler {
88+ return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
89+ filePath := strings .TrimPrefix (r .URL .Path , "/" )
90+ fullPath := dir + "/" + filePath
91+
92+ // Try to load from memory cache
93+ cached , ok := staticFileCache .Load (fullPath )
94+ if ok {
95+ cf := cached .(* cachedFile )
96+
97+ // Check ETag (If-None-Match) and If-Modified-Since
98+ if match := r .Header .Get ("If-None-Match" ); match != "" && match == cf .etag {
99+ w .WriteHeader (http .StatusNotModified )
100+ return
101+ }
102+ if sinceStr := r .Header .Get ("If-Modified-Since" ); sinceStr != "" {
103+ if since , err := time .Parse (http .TimeFormat , sinceStr ); err == nil {
104+ if ! cf .modTime .IsZero () && cf .modTime .Before (since .Add (1 * time .Second )) {
105+ w .WriteHeader (http .StatusNotModified )
106+ return
107+ }
108+ }
109+ }
110+
111+ w .Header ().Set ("ETag" , cf .etag )
112+
113+ // Use bytes.NewReader here to implement io.Seeker properly
114+ http .ServeContent (w , r , filePath , cf .modTime , bytes .NewReader (cf .content ))
115+ return
116+ }
117+
118+ // File not in cache, load from disk
119+ file , err := http .Dir (dir ).Open (filePath )
120+ if err != nil {
121+ http .NotFound (w , r )
122+ return
123+ }
124+ defer file .Close ()
125+
126+ stat , err := file .Stat ()
127+ if err != nil {
128+ http .NotFound (w , r )
129+ return
130+ }
131+
132+ buf := make ([]byte , stat .Size ())
133+ _ , err = file .Read (buf )
134+ if err != nil {
135+ http .NotFound (w , r )
136+ return
137+ }
138+
139+ // Compute ETag and store in cache
140+ etag := computeETag (buf )
141+ cf := & cachedFile {
142+ content : buf ,
143+ modTime : stat .ModTime (),
144+ etag : etag ,
145+ }
146+ staticFileCache .Store (fullPath , cf )
147+
148+ w .Header ().Set ("ETag" , etag )
149+
150+ // Serve using a bytes.Reader to satisfy ServeContent
151+ http .ServeContent (w , r , filePath , cf .modTime , bytes .NewReader (cf .content ))
152+ })
153+ }
154+
18155// createHandler creates the HTTP handler for a site configuration.
19156func createHandler (conf config.SiteConfig , log * logger.Logger , identifier string , globalMwManager * middleware.MiddlewareManager ) (http.Handler , error ) {
20157 var handler http.Handler
@@ -33,10 +170,9 @@ func createHandler(conf config.SiteConfig, log *logger.Logger, identifier string
33170
34171 } else {
35172 // Serve static files from the root directory.
36- fs := http .FileServer (http .Dir (conf .RootDirectory ))
37173 handler = http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
38174 addCustomHeaders (w , conf .CustomHeaders )
39- fs .ServeHTTP (w , r )
175+ memoryCachedFileServer ( conf . RootDirectory , conf . CustomHeaders ) .ServeHTTP (w , r )
40176 })
41177 }
42178
@@ -79,53 +215,3 @@ func addCustomHeaders(w http.ResponseWriter, headers map[string]string) {
79215
80216 w .Header ().Set ("Access-Control-Expose-Headers" , strings .Join (exposeHeaders , ", " ))
81217}
82-
83- var (
84- sharedProxyMap = make (map [string ]* httputil.ReverseProxy )
85- sharedProxyMapMu sync.Mutex
86- defaultTransport = & http.Transport {}
87-
88- globalBytePool = & byteSlicePool {
89- pool : sync.Pool {
90- New : func () interface {} {
91- return make ([]byte , 32 * 1024 )
92- },
93- },
94- }
95- )
96-
97- type byteSlicePool struct {
98- pool sync.Pool
99- }
100-
101- func (b * byteSlicePool ) Get () []byte {
102- return b .pool .Get ().([]byte )
103- }
104-
105- func (b * byteSlicePool ) Put (buf []byte ) {
106- if cap (buf ) == 32 * 1024 {
107- b .pool .Put (buf [:32 * 1024 ])
108- }
109- }
110-
111- // getSharedReverseProxy returns a shared ReverseProxy for the given backend URL.
112- func getSharedReverseProxy (rawURL string ) (* httputil.ReverseProxy , error ) {
113- sharedProxyMapMu .Lock ()
114- defer sharedProxyMapMu .Unlock ()
115-
116- if rp , ok := sharedProxyMap [rawURL ]; ok {
117- return rp , nil
118- }
119-
120- parsedURL , err := url .Parse (rawURL )
121- if err != nil {
122- return nil , err
123- }
124-
125- rp := httputil .NewSingleHostReverseProxy (parsedURL )
126- rp .Transport = defaultTransport
127- rp .BufferPool = globalBytePool
128-
129- sharedProxyMap [rawURL ] = rp
130- return rp , nil
131- }
0 commit comments