mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 17:28:58 +00:00
440 lines
9.3 KiB
Markdown
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.
|