Skip to content

Commit a710dbb

Browse files
authored
feat: add methods to serve static files (#29)
1 parent cce6be6 commit a710dbb

File tree

7 files changed

+215
-0
lines changed

7 files changed

+215
-0
lines changed

kid.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,25 @@ func (k *Kid) ADD(path string, handler HandlerFunc, methods []string, middleware
158158
k.router.add(path, handler, methods, middlewares)
159159
}
160160

161+
// Static registers a new route for serving static files.
162+
//
163+
// It uses http.Dir as its file system.
164+
func (k *Kid) Static(urlPath, staticRoot string, middlewares ...MiddlewareFunc) {
165+
k.StaticFS(urlPath, FS{http.Dir(staticRoot)})
166+
}
167+
168+
// StaticFS registers a new route for serving static files.
169+
//
170+
// It uses the given file system to serve static files.
171+
func (k *Kid) StaticFS(urlPath string, fs http.FileSystem, middlewares ...MiddlewareFunc) {
172+
fileServer := newFileServer(urlPath, fs)
173+
174+
methods := []string{http.MethodGet}
175+
path := appendSlash(urlPath) + "{*filePath}"
176+
177+
k.router.add(path, WrapHandler(fileServer), methods, middlewares)
178+
}
179+
161180
// ServeHTTP implements the http.HandlerFunc interface.
162181
func (k *Kid) ServeHTTP(w http.ResponseWriter, r *http.Request) {
163182
c := k.pool.Get().(*Context)

kid_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,95 @@ func TestKidRun(t *testing.T) {
491491
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
492492
assert.Equal(t, "{\"message\":\"healthy\"}\n", string(body))
493493
}
494+
495+
func TestKidStatic(t *testing.T) {
496+
k := New()
497+
498+
k.Static("/static/", "testdata/static")
499+
500+
testCases := []struct {
501+
name string
502+
req *http.Request
503+
res *httptest.ResponseRecorder
504+
expectedStatusCode int
505+
expectedContent string
506+
}{
507+
{
508+
name: "Serving main.html",
509+
req: httptest.NewRequest(http.MethodGet, "/static/main.html", nil),
510+
res: httptest.NewRecorder(),
511+
expectedStatusCode: http.StatusOK,
512+
expectedContent: "main",
513+
},
514+
{
515+
name: "Serving page.html in pages directory",
516+
req: httptest.NewRequest(http.MethodGet, "/static/pages/page.html", nil),
517+
res: httptest.NewRecorder(),
518+
expectedStatusCode: http.StatusOK,
519+
expectedContent: "page",
520+
},
521+
{
522+
name: "Serving pages/index.html",
523+
req: httptest.NewRequest(http.MethodGet, "/static/pages/", nil),
524+
res: httptest.NewRecorder(),
525+
expectedStatusCode: http.StatusOK,
526+
expectedContent: "index",
527+
},
528+
{
529+
name: "Non-existent",
530+
req: httptest.NewRequest(http.MethodGet, "/static/doesn't-exist.html", nil),
531+
res: httptest.NewRecorder(),
532+
expectedStatusCode: http.StatusNotFound,
533+
expectedContent: "404 page not found\n",
534+
},
535+
}
536+
537+
for _, testCase := range testCases {
538+
t.Run(testCase.name, func(t *testing.T) {
539+
k.ServeHTTP(testCase.res, testCase.req)
540+
541+
assert.Equal(t, testCase.expectedStatusCode, testCase.res.Code)
542+
assert.Equal(t, testCase.expectedContent, testCase.res.Body.String())
543+
})
544+
}
545+
546+
}
547+
548+
func TestKidStaticFS(t *testing.T) {
549+
k := New()
550+
551+
k.StaticFS("/static/", http.Dir("testdata/static"))
552+
553+
testCases := []struct {
554+
name string
555+
req *http.Request
556+
res *httptest.ResponseRecorder
557+
expectedStatusCode int
558+
expectedContent string
559+
}{
560+
{
561+
name: "Serving main.html",
562+
req: httptest.NewRequest(http.MethodGet, "/static/main.html", nil),
563+
res: httptest.NewRecorder(),
564+
expectedStatusCode: http.StatusOK,
565+
expectedContent: "main",
566+
},
567+
{
568+
name: "Serving page.html in pages directory",
569+
req: httptest.NewRequest(http.MethodGet, "/static/pages/page.html", nil),
570+
res: httptest.NewRecorder(),
571+
expectedStatusCode: http.StatusOK,
572+
expectedContent: "page",
573+
},
574+
}
575+
576+
for _, testCase := range testCases {
577+
t.Run(testCase.name, func(t *testing.T) {
578+
k.ServeHTTP(testCase.res, testCase.req)
579+
580+
assert.Equal(t, testCase.expectedStatusCode, testCase.res.Code)
581+
assert.Equal(t, testCase.expectedContent, testCase.res.Body.String())
582+
})
583+
}
584+
585+
}

static.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package kid
2+
3+
import (
4+
"net/http"
5+
"os"
6+
)
7+
8+
// FS is Kid's http.FileSystem implementation to disable directory listing.
9+
type FS struct {
10+
http.FileSystem
11+
}
12+
13+
// File is Kid's http.File implementation to disable directory listing.
14+
type File struct {
15+
http.File
16+
}
17+
18+
// Readdir overrides http.File's default implementation.
19+
//
20+
// It disables directory listing.
21+
func (f File) Readdir(count int) ([]os.FileInfo, error) {
22+
return nil, nil
23+
}
24+
25+
// Open overrides http.FileSystem's default implementation to disable directory listing.
26+
func (fs FS) Open(name string) (http.File, error) {
27+
f, err := fs.FileSystem.Open(name)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
return File{f}, err
33+
}
34+
35+
// newFileServer returns new file server.
36+
func newFileServer(urlPath string, fs http.FileSystem) http.Handler {
37+
if fs == nil {
38+
panic("file system cannot be nil")
39+
}
40+
41+
urlPath = cleanPath(urlPath, false)
42+
urlPath = appendSlash(urlPath)
43+
44+
fileServer := http.StripPrefix(urlPath, http.FileServer(fs))
45+
46+
return fileServer
47+
}
48+
49+
// appendSlash appends slash to a path if the path is not end with slash.
50+
func appendSlash(path string) string {
51+
if path[len(path)-1] != '/' {
52+
path = path + "/"
53+
}
54+
return path
55+
}

static_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package kid
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestAppendSlash(t *testing.T) {
11+
path := "/static"
12+
assert.Equal(t, "/static/", appendSlash(path))
13+
14+
path = path + "/"
15+
assert.Equal(t, "/static/", appendSlash(path))
16+
}
17+
18+
func TestNewFileServer(t *testing.T) {
19+
assert.Panics(t, func() {
20+
newFileServer("/static/", nil)
21+
})
22+
23+
fileServer := newFileServer("/static/", http.Dir("/var/www"))
24+
assert.NotNil(t, fileServer)
25+
}
26+
27+
func TestFileReaddir(t *testing.T) {
28+
var f File
29+
30+
info, err := f.Readdir(10)
31+
32+
assert.Nil(t, info)
33+
assert.Nil(t, err)
34+
}
35+
36+
func TestFSOpen(t *testing.T) {
37+
fs := FS{http.Dir("testdata/static")}
38+
39+
f, err := fs.Open("non-existent.js")
40+
assert.Error(t, err)
41+
assert.Nil(t, f)
42+
43+
f, err = fs.Open("main.html")
44+
assert.NoError(t, err)
45+
assert.IsType(t, File{}, f)
46+
}

testdata/static/main.html

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

testdata/static/pages/index.html

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

testdata/static/pages/page.html

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

0 commit comments

Comments
 (0)