9.3 KiB
StaticWeb - Interface-Driven Static File Server
StaticWeb is a flexible, interface-driven Go package for serving static files over HTTP. It supports multiple filesystem backends (local, zip, embedded) and provides pluggable policies for caching, MIME types, and fallback strategies.
Features
- Router-Agnostic: Works with any HTTP router through standard
http.Handler - Multiple Filesystem Providers: Local directories, zip files, embedded filesystems
- Pluggable Policies: Customizable cache, MIME type, and fallback strategies
- Thread-Safe: Safe for concurrent use
- Resource Management: Proper lifecycle management with
Close()methods - Extensible: Easy to add new providers and policies
Installation
go get github.com/bitechdev/ResolveSpec/pkg/server/staticweb
Quick Start
Basic Usage
Serve files from a local directory:
import "github.com/bitechdev/ResolveSpec/pkg/server/staticweb"
// Create service
service := staticweb.NewService(nil)
// Mount a local directory
provider, _ := staticweb.LocalProvider("./public")
service.Mount(staticweb.MountConfig{
URLPrefix: "/static",
Provider: provider,
})
// Use with any router
router.PathPrefix("/").Handler(service.Handler())
Single Page Application (SPA)
Serve an SPA with HTML fallback routing:
service := staticweb.NewService(nil)
provider, _ := staticweb.LocalProvider("./dist")
service.Mount(staticweb.MountConfig{
URLPrefix: "/",
Provider: provider,
FallbackStrategy: staticweb.HTMLFallback("index.html"),
})
// API routes take precedence (registered first)
router.HandleFunc("/api/users", usersHandler)
router.HandleFunc("/api/posts", postsHandler)
// Static files handle all other routes
router.PathPrefix("/").Handler(service.Handler())
Filesystem Providers
Local Directory
Serve files from a local filesystem directory:
provider, err := staticweb.LocalProvider("/var/www/static")
Zip File
Serve files from a zip archive:
provider, err := staticweb.ZipProvider("./static.zip")
Embedded Filesystem
Serve files from Go's embedded filesystem:
//go:embed assets
var assets embed.FS
// Direct embedded FS
provider, err := staticweb.EmbedProvider(&assets, "")
// Or from a zip file within embedded FS
provider, err := staticweb.EmbedProvider(&assets, "assets.zip")
Cache Policies
Simple Cache
Single TTL for all files:
cachePolicy := staticweb.SimpleCache(3600) // 1 hour
Extension-Based Cache
Different TTLs per file type:
rules := map[string]int{
".html": 3600, // 1 hour
".js": 86400, // 1 day
".css": 86400, // 1 day
".png": 604800, // 1 week
}
cachePolicy := staticweb.ExtensionCache(rules, 3600) // default 1 hour
No Cache
Disable caching entirely:
cachePolicy := staticweb.NoCache()
Fallback Strategies
HTML Fallback
Serve index.html for non-asset requests (SPA routing):
fallback := staticweb.HTMLFallback("index.html")
Extension-Based Fallback
Skip fallback for known static assets:
fallback := staticweb.DefaultExtensionFallback("index.html")
Custom extensions:
staticExts := []string{".js", ".css", ".png", ".jpg"}
fallback := staticweb.ExtensionFallback(staticExts, "index.html")
Configuration
Service Configuration
config := &staticweb.ServiceConfig{
DefaultCacheTime: 3600,
DefaultMIMETypes: map[string]string{
".webp": "image/webp",
".wasm": "application/wasm",
},
}
service := staticweb.NewService(config)
Mount Configuration
service.Mount(staticweb.MountConfig{
URLPrefix: "/static",
Provider: provider,
CachePolicy: cachePolicy, // Optional
MIMEResolver: mimeResolver, // Optional
FallbackStrategy: fallbackStrategy, // Optional
})
Advanced Usage
Multiple Mount Points
Serve different directories at different URL prefixes with different policies:
service := staticweb.NewService(nil)
// Long-lived assets
assetsProvider, _ := staticweb.LocalProvider("./assets")
service.Mount(staticweb.MountConfig{
URLPrefix: "/assets",
Provider: assetsProvider,
CachePolicy: staticweb.SimpleCache(604800), // 1 week
})
// Frequently updated HTML
htmlProvider, _ := staticweb.LocalProvider("./public")
service.Mount(staticweb.MountConfig{
URLPrefix: "/",
Provider: htmlProvider,
CachePolicy: staticweb.SimpleCache(300), // 5 minutes
})
Custom MIME Types
mimeResolver := staticweb.DefaultMIMEResolver()
mimeResolver.RegisterMIMEType(".webp", "image/webp")
mimeResolver.RegisterMIMEType(".wasm", "application/wasm")
service.Mount(staticweb.MountConfig{
URLPrefix: "/static",
Provider: provider,
MIMEResolver: mimeResolver,
})
Resource Cleanup
Always close the service when done:
service := staticweb.NewService(nil)
defer service.Close()
// ... mount and use service ...
Or unmount individual mount points:
service.Unmount("/static")
Reloading/Refreshing Content
Reload providers to pick up changes from the underlying filesystem. This is particularly useful for zip files in development:
// When zip file or directory contents change
err := service.Reload()
if err != nil {
log.Printf("Failed to reload: %v", err)
}
Providers that support reloading:
- ZipFSProvider: Reopens the zip file to pick up changes
- LocalFSProvider: Refreshes the directory view (automatically picks up changes)
- EmbedFSProvider: Not reloadable (embedded at compile time)
You can also reload individual providers:
if reloadable, ok := provider.(staticweb.ReloadableProvider); ok {
err := reloadable.Reload()
if err != nil {
log.Printf("Failed to reload: %v", err)
}
}
Development Workflow Example:
service := staticweb.NewService(nil)
provider, _ := staticweb.ZipProvider("./dist.zip")
service.Mount(staticweb.MountConfig{
URLPrefix: "/app",
Provider: provider,
})
// In development, reload when dist.zip is rebuilt
go func() {
watcher := fsnotify.NewWatcher()
watcher.Add("./dist.zip")
for range watcher.Events {
log.Println("Reloading static files...")
if err := service.Reload(); err != nil {
log.Printf("Reload failed: %v", err)
}
}
}()
Router Integration
Gorilla Mux
router := mux.NewRouter()
router.HandleFunc("/api/users", usersHandler)
router.PathPrefix("/").Handler(service.Handler())
Standard http.ServeMux
http.Handle("/api/", apiHandler)
http.Handle("/", service.Handler())
BunRouter
router.GET("/api/users", usersHandler)
router.GET("/*path", bunrouter.HTTPHandlerFunc(service.Handler()))
Architecture
Core Interfaces
FileSystemProvider
Abstracts the source of files:
type FileSystemProvider interface {
Open(name string) (fs.File, error)
Close() error
Type() string
}
Implementations:
LocalFSProvider- Local directoriesZipFSProvider- Zip archivesEmbedFSProvider- Embedded filesystems
CachePolicy
Defines caching behavior:
type CachePolicy interface {
GetCacheTime(path string) int
GetCacheHeaders(path string) map[string]string
}
Implementations:
SimpleCachePolicy- Single TTLExtensionBasedCachePolicy- Per-extension TTLNoCachePolicy- Disable caching
FallbackStrategy
Handles missing files:
type FallbackStrategy interface {
ShouldFallback(path string) bool
GetFallbackPath(path string) string
}
Implementations:
NoFallback- Return 404HTMLFallbackStrategy- SPA routingExtensionBasedFallback- Skip known assets
MIMETypeResolver
Determines Content-Type:
type MIMETypeResolver interface {
GetMIMEType(path string) string
RegisterMIMEType(extension, mimeType string)
}
Implementations:
DefaultMIMEResolver- Common web typesConfigurableMIMEResolver- Custom mappings
Testing
Mock Providers
import staticwebtesting "github.com/bitechdev/ResolveSpec/pkg/server/staticweb/testing"
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"index.html": []byte("<html>test</html>"),
"app.js": []byte("console.log('test')"),
})
service.Mount(staticweb.MountConfig{
URLPrefix: "/",
Provider: provider,
})
Test Helpers
req := httptest.NewRequest("GET", "/index.html", nil)
rec := httptest.NewRecorder()
service.Handler().ServeHTTP(rec, req)
// Assert response
assert.Equal(t, 200, rec.Code)
Future Features
The interface-driven design allows for easy extensibility:
Planned Providers
- HTTPFSProvider: Fetch files from remote HTTP servers with local caching
- S3FSProvider: Serve files from S3-compatible storage
- CompositeProvider: Fallback chain across multiple providers
- MemoryProvider: In-memory filesystem for testing
Planned Policies
- TimedCachePolicy: Different cache times by time of day
- ConditionalCachePolicy: Smart cache based on file size/type
- RegexFallbackStrategy: Pattern-based routing
License
See the main repository for license information.
Contributing
Contributions are welcome! The interface-driven design makes it easy to add new providers and policies without modifying existing code.