Skip to content

Commit

Permalink
🔍 Refactor static file handling logic in Chi router middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
yarlson committed Jan 29, 2024
1 parent 62cabac commit 1dce386
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 127 deletions.
68 changes: 35 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Chi Static Middleware is a Go package designed to work with the Chi router for s
To install Chi Static Middleware, use the following command:

```bash
go get github.com/yarlson/chistaticmiddleware@v0.2.0
go get github.com/yarlson/chistaticmiddleware@v0.3.0
```

## Usage
Expand All @@ -27,7 +27,7 @@ First, import the package along with Chi:
```go
import (
"github.com/go-chi/chi/v5"
"github.com/yarlson/chistaticmiddleware"
"github.com/yarlson/chistaticmiddleware/static"
)
```

Expand All @@ -36,7 +36,7 @@ import (
Set the cache duration for your static files to control browser caching. This is particularly useful for optimizing load times and reducing server load.

```go
staticConfig := chistaticmiddleware.Config{
staticConfig := static.Config{
// ... other config settings ...
CacheDuration: 24 * time.Hour, // Cache static files for 24 hours
}
Expand All @@ -50,26 +50,25 @@ To serve files from a physical file system, configure the middleware like so:
package main

import (
"github.com/go-chi/chi/v5"
"github.com/yarlson/chistaticmiddleware"
"os"
"time"
"github.com/go-chi/chi/v5"
"github.com/yarlson/chistaticmiddleware/static"
"os"
"time"
)

func main() {
r := chi.NewRouter()
r := chi.NewRouter()

staticConfig := chistaticmiddleware.Config{
StaticFS: os.DirFS("path/to/static/files"),
StaticRoot: "", // use "" for the root
StaticFilePrefix: "/static",
CacheDuration: 24 * time.Hour, // Optional: Cache for 24 hours
}
staticConfig := static.Config{
Fs: os.DirFS("path/to/static/files"),
Root: "", // use "" for the root
FilePrefix: "/static",
CacheDuration: 24 * time.Hour, // Optional: Cache for 24 hours
}

staticMiddleware := chistaticmiddleware.NewStaticMiddleware(staticConfig)
r.Use(staticMiddleware.Handler())
r.Use(static.Handler(staticConfig))

// setup other routes and start server...
// setup other routes and start server...
}
```

Expand All @@ -81,29 +80,28 @@ If you're using Go 1.16 or later, serve static files from an embedded file syste
package main

import (
"embed"
"github.com/go-chi/chi/v5"
"github.com/yarlson/chistaticmiddleware"
"time"
"embed"
"github.com/go-chi/chi/v5"
"github.com/yarlson/chistaticmiddleware/static"
"time"
)

//go:embed path/to/static/files/*
var staticFiles embed.FS

func main() {
r := chi.NewRouter()
r := chi.NewRouter()

staticConfig := chistaticmiddleware.Config{
StaticFS: staticFiles,
StaticRoot: "path/to/static/files",
StaticFilePrefix: "/static",
CacheDuration: 24 * time.Hour, // Optional: Cache for 24 hours
}
staticConfig := static.Config{
Fs: staticFiles,
Root: "path/to/static/files",
FilePrefix: "/static",
CacheDuration: 24 * time.Hour, // Optional: Cache for 24 hours
}

staticMiddleware := chistaticmiddleware.NewStaticMiddleware(staticConfig)
r.Use(staticMiddleware.Handler())
r.Use(static.Handler(staticConfig))

// setup other routes and start server...
// setup other routes and start server...
}
```

Expand All @@ -112,7 +110,7 @@ func main() {
Enable debugging by setting the `Debug` flag in the configuration:

```go
staticConfig := chistaticmiddleware.Config{
staticConfig := static.Config{
// ... other config
Debug: true,
}
Expand All @@ -127,7 +125,11 @@ type CustomLogger struct {
// implementation of the Logger interface
}

staticConfig := chistaticmiddleware.Config{
func (l *CustomLogger) Printf(format string, v ...interface{}) {
// implementation
}

staticConfig := static.Config{
// ... other config
Logger: &CustomLogger{},
}
Expand Down
26 changes: 26 additions & 0 deletions s.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"embed"
"github.com/go-chi/chi/v5"
"github.com/yarlson/chistaticmiddleware/static"
"time"
)

//go:embed path/to/static/files/*
var staticFiles embed.FS

func main() {
r := chi.NewRouter()

staticConfig := static.Config{
Fs: staticFiles,
Root: "path/to/static/files",
FilePrefix: "/static",
CacheDuration: 24 * time.Hour, // Optional: Cache for 24 hours
}

r.Use(static.Handler(staticConfig))

// setup other routes and start server...
}
67 changes: 32 additions & 35 deletions static.go → static/static.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Package chistaticmiddleware provides middleware for the Chi router to serve static files.
// Package static provides middleware for the Chi router to serve static files.
// This package allows detailed configuration including the static file prefix and the root
// directory for static files. It supports both real file systems and embedded file systems.
package chistaticmiddleware
package static

import (
"fmt"
Expand All @@ -21,64 +21,61 @@ type Logger interface {
}

// Config struct defines the configuration for the static file serving middleware.
// StaticFS refers to the file system (which can be embedded) containing the static files.
// StaticRoot specifies the root directory within the file system for the static files.
// StaticFilePrefix is the URL prefix used to serve static files.
// Fs refers to the file system (which can be embedded) containing the static files.
// Root specifies the root directory within the file system for the static files.
// FilePrefix is the URL prefix used to serve static files.
// CacheDuration is the duration for which the static files are cached.
//
// The Debug flag enables additional logging for troubleshooting, and Logger is an interface
// for a custom logging mechanism. If Logger is nil and Debug is true, a default logger is used.
type Config struct {
StaticFS fs.FS
StaticRoot string
StaticFilePrefix string
CacheDuration time.Duration
Fs fs.FS
Root string
FilePrefix string
CacheDuration time.Duration

Debug bool
Logger Logger
}

// StaticMiddleware struct holds the configuration for a middleware instance.
type StaticMiddleware struct {
// middleware struct holds the configuration for a middleware instance.
type middleware struct {
config Config
}

// NewStaticMiddleware initializes a new StaticMiddleware instance with the provided configuration.
// If the Debug flag is set and no custom Logger is provided, it defaults to the standard library's logger.
func NewStaticMiddleware(config Config) *StaticMiddleware {
func Handler(config Config) func(next http.Handler) http.Handler {
if config.Debug && config.Logger == nil {
config.Logger = log.New(os.Stdout, "DEBUG: ", log.LstdFlags)
}
c := &middleware{config: config}

return &StaticMiddleware{config: config}
return c.handler
}

// Handler sets up the HTTP middleware handler. It serves static files based on the URL path
// matching the configured StaticFilePrefix. If the path does not match, it passes the request
// handler sets up the HTTP middleware handler. It serves static files based on the URL path
// matching the configured FilePrefix. If the path does not match, it passes the request
// to the next handler in the middleware chain.
func (m *StaticMiddleware) Handler() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, m.config.StaticFilePrefix) {
if m.config.Debug {
m.config.Logger.Printf("Serving static file: %s", r.URL.Path)
}
m.serveStaticFiles(w, r)
} else {
if m.config.Debug {
m.config.Logger.Printf("Passing request to next handler: %s", r.URL.Path)
}
next.ServeHTTP(w, r)
func (m *middleware) handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, m.config.FilePrefix) {
if m.config.Debug {
m.config.Logger.Printf("Serving static file: %s", r.URL.Path)
}
})
}
m.serveStaticFiles(w, r)
} else {
if m.config.Debug {
m.config.Logger.Printf("Passing request to next handler: %s", r.URL.Path)
}
next.ServeHTTP(w, r)
}
})
}

// serveStaticFiles is responsible for serving the static files. It creates a sub-filesystem
// from the configured static root directory and serves the files using the standard library's
// file server.
func (m *StaticMiddleware) serveStaticFiles(w http.ResponseWriter, r *http.Request) {
staticFS, err := fs.Sub(m.config.StaticFS, m.config.StaticRoot)
func (m *middleware) serveStaticFiles(w http.ResponseWriter, r *http.Request) {
staticFS, err := fs.Sub(m.config.Fs, m.config.Root)
if err != nil {
if m.config.Debug {
m.config.Logger.Printf("Error creating sub-filesystem: %s", err)
Expand All @@ -94,5 +91,5 @@ func (m *StaticMiddleware) serveStaticFiles(w http.ResponseWriter, r *http.Reque
}

fileServer := http.FileServer(http.FS(staticFS))
http.StripPrefix(m.config.StaticFilePrefix, fileServer).ServeHTTP(w, r)
http.StripPrefix(m.config.FilePrefix, fileServer).ServeHTTP(w, r)
}
78 changes: 19 additions & 59 deletions static_test.go → static/static_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package chistaticmiddleware
package static

import (
"github.com/go-chi/chi/v5"
Expand All @@ -9,44 +9,7 @@ import (
"time"
)

// TestNewStaticMiddleware tests the initialization of the StaticMiddleware.
func TestNewStaticMiddleware(t *testing.T) {
tests := []struct {
name string
config Config
wantDebug bool
}{
{
name: "With Debug and No Logger",
config: Config{
Debug: true,
},
wantDebug: true,
},
{
name: "Without Debug and No Logger",
config: Config{
Debug: false,
},
wantDebug: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewStaticMiddleware(tt.config)
if got.config.Debug != tt.wantDebug {
t.Errorf("NewStaticMiddleware().Debug = %v, want %v", got.config.Debug, tt.wantDebug)
}

if tt.wantDebug && got.config.Logger == nil {
t.Errorf("Expected logger to be set when Debug is true")
}
})
}
}

// TestHandler tests the handling of requests by the StaticMiddleware.
// TestHandler tests the handling of requests by the middleware.
func TestHandler(t *testing.T) {
// Create a mock file system using fstest.MapFS
mockFS := fstest.MapFS{
Expand All @@ -58,15 +21,14 @@ func TestHandler(t *testing.T) {

r := chi.NewRouter()
staticConfig := Config{
StaticFS: mockFS,
StaticRoot: "static",
StaticFilePrefix: "/static",
Debug: true,
CacheDuration: 365 * 24 * time.Hour,
Fs: mockFS,
Root: "static",
FilePrefix: "/static",
Debug: true,
CacheDuration: 365 * 24 * time.Hour,
}
staticMiddleware := NewStaticMiddleware(staticConfig)

r.Use(staticMiddleware.Handler())
r.Use(Handler(staticConfig))

// Next handler for non-static routes
nextHandlerCalled := false
Expand Down Expand Up @@ -107,7 +69,7 @@ func TestHandler(t *testing.T) {
}
}

// TestHandler404 tests the handling of requests by the StaticMiddleware when the requested file does not exist.
// TestHandler404 tests the handling of requests by the middleware when the requested file does not exist.
func TestHandler404(t *testing.T) {
// Create a mock file system using fstest.MapFS
mockFS := fstest.MapFS{
Expand All @@ -119,13 +81,12 @@ func TestHandler404(t *testing.T) {

r := chi.NewRouter()
staticConfig := Config{
StaticFS: mockFS,
StaticRoot: "static",
StaticFilePrefix: "/static",
Fs: mockFS,
Root: "static",
FilePrefix: "/static",
}
staticMiddleware := NewStaticMiddleware(staticConfig)

r.Use(staticMiddleware.Handler())
r.Use(Handler(staticConfig))

r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Expected static handler to be called for matching path")
Expand All @@ -146,7 +107,7 @@ func TestHandler404(t *testing.T) {
}
}

// TestHandlerError tests the behavior of the Handler function when fs.Sub(m.config.StaticFS, m.config.StaticRoot) raises an error.
// TestHandlerError tests the behavior of the Handler function when fs.Sub(m.config.Fs, m.config.Root) raises an error.
func TestHandlerError(t *testing.T) {
// Create a mock file system using fstest.MapFS
mockFS := fstest.MapFS{
Expand All @@ -158,14 +119,13 @@ func TestHandlerError(t *testing.T) {

r := chi.NewRouter()
staticConfig := Config{
StaticFS: mockFS,
StaticRoot: "./static",
StaticFilePrefix: "/static",
Debug: true,
Fs: mockFS,
Root: "./static",
FilePrefix: "/static",
Debug: true,
}
staticMiddleware := NewStaticMiddleware(staticConfig)

r.Use(staticMiddleware.Handler())
r.Use(Handler(staticConfig))

r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Expected static handler to be called for matching path")
Expand Down

0 comments on commit 1dce386

Please sign in to comment.