diff --git a/pkg/server/staticweb/interfaces.go b/pkg/server/staticweb/interfaces.go index 612fc20..6a71f4b 100644 --- a/pkg/server/staticweb/interfaces.go +++ b/pkg/server/staticweb/interfaces.go @@ -34,6 +34,29 @@ type ReloadableProvider interface { 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. // Implementations must be safe for concurrent use. type CachePolicy interface { diff --git a/pkg/server/staticweb/providers/embed.go b/pkg/server/staticweb/providers/embed.go index 58712d8..651cc41 100644 --- a/pkg/server/staticweb/providers/embed.go +++ b/pkg/server/staticweb/providers/embed.go @@ -142,12 +142,10 @@ func (p *EmbedFSProvider) ZipFile() string { // WithStripPrefix sets the prefix to strip from requested paths. // For example, WithStripPrefix("/dist") will make files at "/dist/assets" // accessible via "/assets". -// Returns the provider for method chaining. -func (p *EmbedFSProvider) WithStripPrefix(prefix string) *EmbedFSProvider { +func (p *EmbedFSProvider) WithStripPrefix(prefix string) { p.mu.Lock() defer p.mu.Unlock() p.stripPrefix = prefix - return p } // StripPrefix returns the configured strip prefix. diff --git a/pkg/server/staticweb/providers/local.go b/pkg/server/staticweb/providers/local.go index cdb05a8..24f718f 100644 --- a/pkg/server/staticweb/providers/local.go +++ b/pkg/server/staticweb/providers/local.go @@ -4,13 +4,18 @@ import ( "fmt" "io/fs" "os" + "path" "path/filepath" + "strings" + "sync" ) // LocalFSProvider serves files from a local directory. type LocalFSProvider struct { - path string - fs fs.FS + path string + stripPrefix string + fs fs.FS + mu sync.RWMutex } // 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. +// 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) { - 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. @@ -63,6 +88,9 @@ func (p *LocalFSProvider) Path() string { // For local directories, os.DirFS automatically picks up changes, // so this recreates the DirFS to ensure a fresh view. func (p *LocalFSProvider) Reload() error { + p.mu.Lock() + defer p.mu.Unlock() + // Verify the directory still exists info, err := os.Stat(p.path) if err != nil { @@ -78,3 +106,19 @@ func (p *LocalFSProvider) Reload() error { 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 +} diff --git a/pkg/server/staticweb/providers/zip.go b/pkg/server/staticweb/providers/zip.go index cf9de84..363448c 100644 --- a/pkg/server/staticweb/providers/zip.go +++ b/pkg/server/staticweb/providers/zip.go @@ -4,7 +4,9 @@ import ( "archive/zip" "fmt" "io/fs" + "path" "path/filepath" + "strings" "sync" "github.com/bitechdev/ResolveSpec/pkg/server/zipfs" @@ -12,10 +14,11 @@ import ( // ZipFSProvider serves files from a zip file. type ZipFSProvider struct { - zipPath string - zipReader *zip.ReadCloser - zipFS *zipfs.ZipFS - mu sync.RWMutex + zipPath string + stripPrefix string + zipReader *zip.ReadCloser + zipFS *zipfs.ZipFS + mu sync.RWMutex } // 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. +// 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) { p.mu.RLock() 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 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. @@ -100,3 +120,19 @@ func (p *ZipFSProvider) Reload() error { 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 +}