mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 00:34:25 +00:00
190 lines
4.4 KiB
Go
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
|
|
}
|