Files
ResolveSpec/pkg/server/staticweb/service.go
2025-12-30 17:46:33 +02:00

190 lines
4.4 KiB
Go

package staticweb
import (
"fmt"
"net/http"
"sort"
"strings"
"sync"
)
// service implements the StaticFileService interface.
type service struct {
mounts map[string]*mountPoint // urlPrefix -> mount point
config *ServiceConfig
mu sync.RWMutex
}
// NewService creates a new static file service with the given configuration.
// If config is nil, default configuration is used.
func NewService(config *ServiceConfig) StaticFileService {
if config == nil {
config = DefaultServiceConfig()
}
return &service{
mounts: make(map[string]*mountPoint),
config: config,
}
}
// Mount adds a new mount point with the given configuration.
func (s *service) Mount(config MountConfig) error {
// Validate the config
if err := s.validateMountConfig(config); err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
// Check if the prefix is already mounted
if _, exists := s.mounts[config.URLPrefix]; exists {
return fmt.Errorf("mount point already exists at %s", config.URLPrefix)
}
// Create the mount point
mp, err := newMountPoint(config, s.config)
if err != nil {
return fmt.Errorf("failed to create mount point: %w", err)
}
// Add to the map
s.mounts[config.URLPrefix] = mp
return nil
}
// Unmount removes the mount point at the given URL prefix.
func (s *service) Unmount(urlPrefix string) error {
s.mu.Lock()
defer s.mu.Unlock()
mp, exists := s.mounts[urlPrefix]
if !exists {
return fmt.Errorf("no mount point exists at %s", urlPrefix)
}
// Close the mount point to release resources
if err := mp.Close(); err != nil {
return fmt.Errorf("failed to close mount point: %w", err)
}
// Remove from the map
delete(s.mounts, urlPrefix)
return nil
}
// ListMounts returns a sorted list of all mounted URL prefixes.
func (s *service) ListMounts() []string {
s.mu.RLock()
defer s.mu.RUnlock()
prefixes := make([]string, 0, len(s.mounts))
for prefix := range s.mounts {
prefixes = append(prefixes, prefix)
}
sort.Strings(prefixes)
return prefixes
}
// Reload reinitializes all filesystem providers that support reloading.
// This is useful when the underlying files have changed (e.g., zip file updated).
// Providers that implement ReloadableProvider will be reloaded.
func (s *service) Reload() error {
s.mu.RLock()
defer s.mu.RUnlock()
var errors []error
// Reload all mount points that support it
for prefix, mp := range s.mounts {
if reloadable, ok := mp.provider.(ReloadableProvider); ok {
if err := reloadable.Reload(); err != nil {
errors = append(errors, fmt.Errorf("%s: %w", prefix, err))
}
}
}
// Return combined errors if any
if len(errors) > 0 {
return fmt.Errorf("errors while reloading providers: %v", errors)
}
return nil
}
// Close releases all resources held by the service.
func (s *service) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
var errors []error
// Close all mount points
for prefix, mp := range s.mounts {
if err := mp.Close(); err != nil {
errors = append(errors, fmt.Errorf("%s: %w", prefix, err))
}
}
// Clear the map
s.mounts = make(map[string]*mountPoint)
// Return combined errors if any
if len(errors) > 0 {
return fmt.Errorf("errors while closing mount points: %v", errors)
}
return nil
}
// Handler returns an http.Handler that serves static files from all mount points.
func (s *service) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
// Find the best matching mount point using longest-prefix matching
var bestMatch *mountPoint
var bestPrefix string
for prefix, mp := range s.mounts {
if strings.HasPrefix(r.URL.Path, prefix) {
if len(prefix) > len(bestPrefix) {
bestMatch = mp
bestPrefix = prefix
}
}
}
// If no mount point matches, return without writing a response
// This allows other handlers (like API routes) to process the request
if bestMatch == nil {
return
}
// Serve the file from the matched mount point
bestMatch.ServeHTTP(w, r)
})
}
// validateMountConfig validates the mount configuration.
func (s *service) validateMountConfig(config MountConfig) error {
if config.URLPrefix == "" {
return fmt.Errorf("URLPrefix cannot be empty")
}
if !strings.HasPrefix(config.URLPrefix, "/") {
return fmt.Errorf("URLPrefix must start with /")
}
if config.Provider == nil {
return fmt.Errorf("provider cannot be nil")
}
return nil
}