Update media handling
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
webbundle "luxtools/web"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,45 +28,53 @@ const (
|
||||
`
|
||||
)
|
||||
|
||||
// Config contains the runtime options for the HTTP server.
|
||||
type Config struct {
|
||||
MediaDir string
|
||||
}
|
||||
|
||||
// Server hosts the Luxtools HTTP handlers.
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
indexPage []byte
|
||||
staticRoot string
|
||||
assetsRoot string
|
||||
mux *http.ServeMux
|
||||
indexPage []byte
|
||||
staticFS fs.FS
|
||||
mediaFS http.FileSystem
|
||||
|
||||
counterMu sync.Mutex
|
||||
clickCount int
|
||||
}
|
||||
|
||||
// New constructs a configured Server ready to serve requests.
|
||||
func New() (*Server, error) {
|
||||
projectRoot, err := goProjectRoot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func New(cfg Config) (*Server, error) {
|
||||
if strings.TrimSpace(cfg.MediaDir) == "" {
|
||||
return nil, fmt.Errorf("media directory is required")
|
||||
}
|
||||
|
||||
indexPath := filepath.Join(projectRoot, "web", "templates", "index.html")
|
||||
staticDir := filepath.Join(projectRoot, "web", "static")
|
||||
assetsDir := filepath.Join(projectRoot, "assets")
|
||||
mediaDir := filepath.Clean(cfg.MediaDir)
|
||||
|
||||
indexPage, err := os.ReadFile(indexPath)
|
||||
info, err := os.Stat(mediaDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("media directory check failed: %w", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, fmt.Errorf("media directory must be a directory: %s", mediaDir)
|
||||
}
|
||||
|
||||
indexPage, err := webbundle.Content.ReadFile("templates/index.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read index template: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(staticDir); err != nil {
|
||||
return nil, fmt.Errorf("static directory check failed: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(assetsDir); err != nil {
|
||||
return nil, fmt.Errorf("assets directory check failed: %w", err)
|
||||
staticFS, err := fs.Sub(webbundle.Content, "static")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare static assets: %w", err)
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
mux: http.NewServeMux(),
|
||||
indexPage: indexPage,
|
||||
staticRoot: staticDir,
|
||||
assetsRoot: assetsDir,
|
||||
mux: http.NewServeMux(),
|
||||
indexPage: indexPage,
|
||||
staticFS: staticFS,
|
||||
mediaFS: http.Dir(mediaDir),
|
||||
}
|
||||
|
||||
s.registerRoutes()
|
||||
@@ -82,8 +92,11 @@ func (s *Server) Address() string {
|
||||
}
|
||||
|
||||
func (s *Server) registerRoutes() {
|
||||
s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(http.FileServer(http.Dir(s.staticRoot)))))
|
||||
s.mux.Handle("/assets/", http.StripPrefix("/assets/", allowGetHead(http.FileServer(http.Dir(s.assetsRoot)))))
|
||||
staticHandler := http.FileServer(http.FS(s.staticFS))
|
||||
mediaHandler := http.FileServer(s.mediaFS)
|
||||
|
||||
s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(staticHandler)))
|
||||
s.mux.Handle("/media/", http.StripPrefix("/media/", allowGetHead(mediaHandler)))
|
||||
s.mux.HandleFunc("/", s.handleRequest)
|
||||
}
|
||||
|
||||
@@ -157,14 +170,3 @@ func allowGetHead(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func goProjectRoot() (string, error) {
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
return "", errors.New("unable to determine caller information")
|
||||
}
|
||||
|
||||
dir := filepath.Dir(currentFile)
|
||||
projectRoot := filepath.Clean(filepath.Join(dir, "..", ".."))
|
||||
return projectRoot, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -28,10 +30,7 @@ func TestRenderTime(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCounterEndpoint(t *testing.T) {
|
||||
srv, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
srv := mustNewServer(t, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/counter", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
@@ -49,10 +48,7 @@ func TestCounterEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
srv, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
srv := mustNewServer(t, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/unknown", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
@@ -66,3 +62,68 @@ func TestNotFound(t *testing.T) {
|
||||
t.Fatalf("expected not found message, got %q", resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFileServing(t *testing.T) {
|
||||
srv := mustNewServer(t, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/static/lib/htmx.2.0.7.min.js", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
srv.Router().ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("expected static asset to load, got status %d", resp.Code)
|
||||
}
|
||||
if resp.Body.Len() == 0 {
|
||||
t.Fatalf("expected static asset response body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaServing(t *testing.T) {
|
||||
var demoName string
|
||||
srv := mustNewServer(t, func(dir string) {
|
||||
demoName = filepath.Join(dir, "demo.txt")
|
||||
if err := os.WriteFile(demoName, []byte("demo media file"), 0o644); err != nil {
|
||||
t.Fatalf("failed to seed media file: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/media/demo.txt", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
srv.Router().ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("expected public asset to load, got status %d", resp.Code)
|
||||
}
|
||||
body := resp.Body.String()
|
||||
if !strings.Contains(body, "demo media file") {
|
||||
t.Fatalf("unexpected asset body: %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRequiresMediaDir(t *testing.T) {
|
||||
if _, err := New(Config{}); err == nil {
|
||||
t.Fatalf("expected error when media dir is empty")
|
||||
}
|
||||
|
||||
missing := filepath.Join(t.TempDir(), "missing")
|
||||
if _, err := New(Config{MediaDir: missing}); err == nil {
|
||||
t.Fatalf("expected error when media dir does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewServer(t *testing.T, setup func(string)) *Server {
|
||||
t.Helper()
|
||||
|
||||
mediaDir := t.TempDir()
|
||||
if setup != nil {
|
||||
setup(mediaDir)
|
||||
}
|
||||
|
||||
srv, err := New(Config{MediaDir: mediaDir})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user