feat: add Static and StaticFS helpers for serving static files
One-liner static file serving: v.Static("/assets/", "./public") for
filesystem directories and v.StaticFS("/assets/", fsys) for embed.FS.
Both auto-normalize the URL prefix and disable directory listings.
This commit is contained in:
143
static_test.go
Normal file
143
static_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package via
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStatic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.MkdirAll(filepath.Join(dir, "sub"), 0755)
|
||||
os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello world"), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "sub", "nested.txt"), []byte("nested"), 0644)
|
||||
|
||||
v := New()
|
||||
v.Static("/assets/", dir)
|
||||
|
||||
t.Run("serves file", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/assets/hello.txt", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "hello world", w.Body.String())
|
||||
})
|
||||
|
||||
t.Run("serves nested file", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/assets/sub/nested.txt", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "nested", w.Body.String())
|
||||
})
|
||||
|
||||
t.Run("directory listing returns 404", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/assets/", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
|
||||
t.Run("subdirectory listing returns 404", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/assets/sub/", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
|
||||
t.Run("missing file returns 404", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/assets/nope.txt", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStaticAutoSlash(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(dir, "ok.txt"), []byte("ok"), 0644)
|
||||
|
||||
v := New()
|
||||
v.Static("/files", dir) // no trailing slash
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/files/ok.txt", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "ok", w.Body.String())
|
||||
}
|
||||
|
||||
func TestStaticFS(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"style.css": {Data: []byte("body{}")},
|
||||
"js/app.js": {Data: []byte("console.log('hi')")},
|
||||
}
|
||||
|
||||
v := New()
|
||||
v.StaticFS("/static/", fsys)
|
||||
|
||||
t.Run("serves file", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/static/style.css", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "body{}", w.Body.String())
|
||||
})
|
||||
|
||||
t.Run("serves nested file", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/static/js/app.js", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "console.log('hi')", w.Body.String())
|
||||
})
|
||||
|
||||
t.Run("directory listing returns 404", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/static/", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
|
||||
t.Run("missing file returns 404", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/static/nope.css", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStaticFSAutoSlash(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"ok.txt": {Data: []byte("ok")},
|
||||
}
|
||||
|
||||
v := New()
|
||||
v.StaticFS("/embed", fsys) // no trailing slash
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/embed/ok.txt", nil)
|
||||
v.mux.ServeHTTP(w, r)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "ok", w.Body.String())
|
||||
}
|
||||
|
||||
// Verify StaticFS accepts the fs.FS interface (compile-time check).
|
||||
var _ fs.FS = fstest.MapFS{}
|
||||
43
via.go
43
via.go
@@ -9,12 +9,13 @@ package via
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"crypto/subtle"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -412,6 +413,46 @@ func (v *V) HTTPServeMux() *http.ServeMux {
|
||||
return v.mux
|
||||
}
|
||||
|
||||
// Static serves files from a filesystem directory at the given URL prefix.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// v.Static("/assets/", "./public")
|
||||
func (v *V) Static(urlPrefix, dir string) {
|
||||
if !strings.HasSuffix(urlPrefix, "/") {
|
||||
urlPrefix += "/"
|
||||
}
|
||||
fileServer := http.StripPrefix(urlPrefix, http.FileServer(http.Dir(dir)))
|
||||
v.mux.Handle("GET "+urlPrefix, noDirListing(fileServer))
|
||||
}
|
||||
|
||||
// StaticFS serves files from an [fs.FS] at the given URL prefix.
|
||||
// This is useful with //go:embed filesystems.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// //go:embed static
|
||||
// var staticFiles embed.FS
|
||||
// v.StaticFS("/assets/", staticFiles)
|
||||
func (v *V) StaticFS(urlPrefix string, fsys fs.FS) {
|
||||
if !strings.HasSuffix(urlPrefix, "/") {
|
||||
urlPrefix += "/"
|
||||
}
|
||||
fileServer := http.StripPrefix(urlPrefix, http.FileServerFS(fsys))
|
||||
v.mux.Handle("GET "+urlPrefix, noDirListing(fileServer))
|
||||
}
|
||||
|
||||
// noDirListing wraps a file server handler to return 404 for directory requests.
|
||||
func noDirListing(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (v *V) ensureDatastarHandler() {
|
||||
v.datastarOnce.Do(func() {
|
||||
v.mux.HandleFunc("GET "+v.datastarPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user