Files
ResolveSpec/pkg/server/staticweb/README.md

440 lines
9.3 KiB
Markdown

# 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
```bash
go get github.com/bitechdev/ResolveSpec/pkg/server/staticweb
```
## Quick Start
### Basic Usage
Serve files from a local directory:
```go
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:
```go
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:
```go
provider, err := staticweb.LocalProvider("/var/www/static")
```
### Zip File
Serve files from a zip archive:
```go
provider, err := staticweb.ZipProvider("./static.zip")
```
### Embedded Filesystem
Serve files from Go's embedded filesystem:
```go
//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:
```go
cachePolicy := staticweb.SimpleCache(3600) // 1 hour
```
### Extension-Based Cache
Different TTLs per file type:
```go
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:
```go
cachePolicy := staticweb.NoCache()
```
## Fallback Strategies
### HTML Fallback
Serve index.html for non-asset requests (SPA routing):
```go
fallback := staticweb.HTMLFallback("index.html")
```
### Extension-Based Fallback
Skip fallback for known static assets:
```go
fallback := staticweb.DefaultExtensionFallback("index.html")
```
Custom extensions:
```go
staticExts := []string{".js", ".css", ".png", ".jpg"}
fallback := staticweb.ExtensionFallback(staticExts, "index.html")
```
## Configuration
### Service Configuration
```go
config := &staticweb.ServiceConfig{
DefaultCacheTime: 3600,
DefaultMIMETypes: map[string]string{
".webp": "image/webp",
".wasm": "application/wasm",
},
}
service := staticweb.NewService(config)
```
### Mount Configuration
```go
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:
```go
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
```go
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:
```go
service := staticweb.NewService(nil)
defer service.Close()
// ... mount and use service ...
```
Or unmount individual mount points:
```go
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:
```go
// 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:
```go
if reloadable, ok := provider.(staticweb.ReloadableProvider); ok {
err := reloadable.Reload()
if err != nil {
log.Printf("Failed to reload: %v", err)
}
}
```
**Development Workflow Example:**
```go
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
```go
router := mux.NewRouter()
router.HandleFunc("/api/users", usersHandler)
router.PathPrefix("/").Handler(service.Handler())
```
### Standard http.ServeMux
```go
http.Handle("/api/", apiHandler)
http.Handle("/", service.Handler())
```
### BunRouter
```go
router.GET("/api/users", usersHandler)
router.GET("/*path", bunrouter.HTTPHandlerFunc(service.Handler()))
```
## Architecture
### Core Interfaces
#### FileSystemProvider
Abstracts the source of files:
```go
type FileSystemProvider interface {
Open(name string) (fs.File, error)
Close() error
Type() string
}
```
Implementations:
- `LocalFSProvider` - Local directories
- `ZipFSProvider` - Zip archives
- `EmbedFSProvider` - Embedded filesystems
#### CachePolicy
Defines caching behavior:
```go
type CachePolicy interface {
GetCacheTime(path string) int
GetCacheHeaders(path string) map[string]string
}
```
Implementations:
- `SimpleCachePolicy` - Single TTL
- `ExtensionBasedCachePolicy` - Per-extension TTL
- `NoCachePolicy` - Disable caching
#### FallbackStrategy
Handles missing files:
```go
type FallbackStrategy interface {
ShouldFallback(path string) bool
GetFallbackPath(path string) string
}
```
Implementations:
- `NoFallback` - Return 404
- `HTMLFallbackStrategy` - SPA routing
- `ExtensionBasedFallback` - Skip known assets
#### MIMETypeResolver
Determines Content-Type:
```go
type MIMETypeResolver interface {
GetMIMEType(path string) string
RegisterMIMEType(extension, mimeType string)
}
```
Implementations:
- `DefaultMIMEResolver` - Common web types
- `ConfigurableMIMEResolver` - Custom mappings
## Testing
### Mock Providers
```go
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
```go
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.