Compare commits

..

1 Commits

Author SHA1 Message Date
2017465cb8 feat(staticweb): add path prefix stripping to all filesystem providers
Add PrefixStrippingProvider interface and implement it in all providers
(EmbedFSProvider, LocalFSProvider, ZipFSProvider) to support serving
files from subdirectories at the root level.
2026-01-03 14:39:51 +02:00
4 changed files with 112 additions and 11 deletions

View File

@@ -34,6 +34,29 @@ type ReloadableProvider interface {
Reload() error Reload() error
} }
// PrefixStrippingProvider is an optional interface that providers can implement
// to support stripping path prefixes from requested paths.
// This is useful when files are stored in a subdirectory but should be accessible
// at the root level (e.g., files at "/dist/assets" accessible via "/assets").
type PrefixStrippingProvider interface {
// WithStripPrefix sets the prefix to strip from requested paths.
// For example, WithStripPrefix("/dist") will make files at "/dist/assets"
// accessible via "/assets".
WithStripPrefix(prefix string)
// StripPrefix returns the configured strip prefix.
StripPrefix() string
}
// WithStripPrefix is a helper function that sets the strip prefix on a provider
// if it implements PrefixStrippingProvider. Returns the provider for method chaining.
func WithStripPrefix(provider FileSystemProvider, prefix string) FileSystemProvider {
if p, ok := provider.(PrefixStrippingProvider); ok {
p.WithStripPrefix(prefix)
}
return provider
}
// CachePolicy defines how files should be cached by browsers and proxies. // CachePolicy defines how files should be cached by browsers and proxies.
// Implementations must be safe for concurrent use. // Implementations must be safe for concurrent use.
type CachePolicy interface { type CachePolicy interface {

View File

@@ -142,12 +142,10 @@ func (p *EmbedFSProvider) ZipFile() string {
// WithStripPrefix sets the prefix to strip from requested paths. // WithStripPrefix sets the prefix to strip from requested paths.
// For example, WithStripPrefix("/dist") will make files at "/dist/assets" // For example, WithStripPrefix("/dist") will make files at "/dist/assets"
// accessible via "/assets". // accessible via "/assets".
// Returns the provider for method chaining. func (p *EmbedFSProvider) WithStripPrefix(prefix string) {
func (p *EmbedFSProvider) WithStripPrefix(prefix string) *EmbedFSProvider {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
p.stripPrefix = prefix p.stripPrefix = prefix
return p
} }
// StripPrefix returns the configured strip prefix. // StripPrefix returns the configured strip prefix.

View File

@@ -4,13 +4,18 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings"
"sync"
) )
// LocalFSProvider serves files from a local directory. // LocalFSProvider serves files from a local directory.
type LocalFSProvider struct { type LocalFSProvider struct {
path string path string
fs fs.FS stripPrefix string
fs fs.FS
mu sync.RWMutex
} }
// NewLocalFSProvider creates a new LocalFSProvider for the given directory path. // NewLocalFSProvider creates a new LocalFSProvider for the given directory path.
@@ -39,8 +44,28 @@ func NewLocalFSProvider(path string) (*LocalFSProvider, error) {
} }
// Open opens the named file from the local directory. // Open opens the named file from the local directory.
// If a strip prefix is configured, it prepends the prefix to the requested path.
// For example, with stripPrefix="/dist", requesting "/assets/style.css" will
// open "/dist/assets/style.css" from the local filesystem.
func (p *LocalFSProvider) Open(name string) (fs.File, error) { func (p *LocalFSProvider) Open(name string) (fs.File, error) {
return p.fs.Open(name) p.mu.RLock()
defer p.mu.RUnlock()
// Apply prefix stripping by prepending the prefix to the requested path
actualPath := name
if p.stripPrefix != "" {
// Clean the paths to handle leading/trailing slashes
prefix := strings.Trim(p.stripPrefix, "/")
cleanName := strings.TrimPrefix(name, "/")
if prefix != "" {
actualPath = path.Join(prefix, cleanName)
} else {
actualPath = cleanName
}
}
return p.fs.Open(actualPath)
} }
// Close releases any resources held by the provider. // Close releases any resources held by the provider.
@@ -63,6 +88,9 @@ func (p *LocalFSProvider) Path() string {
// For local directories, os.DirFS automatically picks up changes, // For local directories, os.DirFS automatically picks up changes,
// so this recreates the DirFS to ensure a fresh view. // so this recreates the DirFS to ensure a fresh view.
func (p *LocalFSProvider) Reload() error { func (p *LocalFSProvider) Reload() error {
p.mu.Lock()
defer p.mu.Unlock()
// Verify the directory still exists // Verify the directory still exists
info, err := os.Stat(p.path) info, err := os.Stat(p.path)
if err != nil { if err != nil {
@@ -78,3 +106,19 @@ func (p *LocalFSProvider) Reload() error {
return nil return nil
} }
// WithStripPrefix sets the prefix to strip from requested paths.
// For example, WithStripPrefix("/dist") will make files at "/dist/assets"
// accessible via "/assets".
func (p *LocalFSProvider) WithStripPrefix(prefix string) {
p.mu.Lock()
defer p.mu.Unlock()
p.stripPrefix = prefix
}
// StripPrefix returns the configured strip prefix.
func (p *LocalFSProvider) StripPrefix() string {
p.mu.RLock()
defer p.mu.RUnlock()
return p.stripPrefix
}

View File

@@ -4,7 +4,9 @@ import (
"archive/zip" "archive/zip"
"fmt" "fmt"
"io/fs" "io/fs"
"path"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"github.com/bitechdev/ResolveSpec/pkg/server/zipfs" "github.com/bitechdev/ResolveSpec/pkg/server/zipfs"
@@ -12,10 +14,11 @@ import (
// ZipFSProvider serves files from a zip file. // ZipFSProvider serves files from a zip file.
type ZipFSProvider struct { type ZipFSProvider struct {
zipPath string zipPath string
zipReader *zip.ReadCloser stripPrefix string
zipFS *zipfs.ZipFS zipReader *zip.ReadCloser
mu sync.RWMutex zipFS *zipfs.ZipFS
mu sync.RWMutex
} }
// NewZipFSProvider creates a new ZipFSProvider for the given zip file path. // NewZipFSProvider creates a new ZipFSProvider for the given zip file path.
@@ -40,6 +43,9 @@ func NewZipFSProvider(zipPath string) (*ZipFSProvider, error) {
} }
// Open opens the named file from the zip archive. // Open opens the named file from the zip archive.
// If a strip prefix is configured, it prepends the prefix to the requested path.
// For example, with stripPrefix="/dist", requesting "/assets/style.css" will
// open "/dist/assets/style.css" from the zip filesystem.
func (p *ZipFSProvider) Open(name string) (fs.File, error) { func (p *ZipFSProvider) Open(name string) (fs.File, error) {
p.mu.RLock() p.mu.RLock()
defer p.mu.RUnlock() defer p.mu.RUnlock()
@@ -48,7 +54,21 @@ func (p *ZipFSProvider) Open(name string) (fs.File, error) {
return nil, fmt.Errorf("zip filesystem is closed") return nil, fmt.Errorf("zip filesystem is closed")
} }
return p.zipFS.Open(name) // Apply prefix stripping by prepending the prefix to the requested path
actualPath := name
if p.stripPrefix != "" {
// Clean the paths to handle leading/trailing slashes
prefix := strings.Trim(p.stripPrefix, "/")
cleanName := strings.TrimPrefix(name, "/")
if prefix != "" {
actualPath = path.Join(prefix, cleanName)
} else {
actualPath = cleanName
}
}
return p.zipFS.Open(actualPath)
} }
// Close releases resources held by the zip reader. // Close releases resources held by the zip reader.
@@ -100,3 +120,19 @@ func (p *ZipFSProvider) Reload() error {
return nil return nil
} }
// WithStripPrefix sets the prefix to strip from requested paths.
// For example, WithStripPrefix("/dist") will make files at "/dist/assets"
// accessible via "/assets".
func (p *ZipFSProvider) WithStripPrefix(prefix string) {
p.mu.Lock()
defer p.mu.Unlock()
p.stripPrefix = prefix
}
// StripPrefix returns the configured strip prefix.
func (p *ZipFSProvider) StripPrefix() string {
p.mu.RLock()
defer p.mu.RUnlock()
return p.stripPrefix
}