Files
ResolveSpec/pkg/server/staticweb/providers/zip.go
Hein 0d50bcfee6 fix(provider): enhance file opening logic with alternate path. Handling broken cases to be compatible with Bitech clients
* Implemented alternate path handling for file retrieval
* Improved error messaging for file not found scenarios
2026-03-24 09:02:17 +02:00

154 lines
3.7 KiB
Go

package providers
import (
"archive/zip"
"fmt"
"io/fs"
"path"
"path/filepath"
"strings"
"sync"
"github.com/bitechdev/ResolveSpec/pkg/server/zipfs"
)
// ZipFSProvider serves files from a zip file.
type ZipFSProvider struct {
zipPath string
stripPrefix string
zipReader *zip.ReadCloser
zipFS *zipfs.ZipFS
mu sync.RWMutex
}
// NewZipFSProvider creates a new ZipFSProvider for the given zip file path.
func NewZipFSProvider(zipPath string) (*ZipFSProvider, error) {
// Convert to absolute path
absPath, err := filepath.Abs(zipPath)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path: %w", err)
}
// Open the zip file
zipReader, err := zip.OpenReader(absPath)
if err != nil {
return nil, fmt.Errorf("failed to open zip file: %w", err)
}
return &ZipFSProvider{
zipPath: absPath,
zipReader: zipReader,
zipFS: zipfs.NewZipFS(&zipReader.Reader),
}, nil
}
// 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()
if p.zipFS == nil {
return nil, fmt.Errorf("zip filesystem is closed")
}
// Apply prefix stripping by prepending the prefix to the requested path
actualPath := name
alternatePath := ""
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)
alternatePath = cleanName
} else {
actualPath = cleanName
}
}
// First try the actual path with prefix
if file, err := p.zipFS.Open(actualPath); err == nil {
return file, nil
}
// If alternate path is different, try it as well
if alternatePath != "" && alternatePath != actualPath {
if file, err := p.zipFS.Open(alternatePath); err == nil {
return file, nil
}
}
// If both attempts fail, return the error from the first attempt
return nil, fmt.Errorf("file not found: %s", name)
}
// Close releases resources held by the zip reader.
func (p *ZipFSProvider) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.zipReader != nil {
err := p.zipReader.Close()
p.zipReader = nil
p.zipFS = nil
return err
}
return nil
}
// Type returns "zip".
func (p *ZipFSProvider) Type() string {
return "zip"
}
// Path returns the absolute path to the zip file being served.
func (p *ZipFSProvider) Path() string {
return p.zipPath
}
// Reload reopens the zip file to pick up any changes.
// This is useful in development when the zip file is updated.
func (p *ZipFSProvider) Reload() error {
p.mu.Lock()
defer p.mu.Unlock()
// Close the existing zip reader if open
if p.zipReader != nil {
if err := p.zipReader.Close(); err != nil {
return fmt.Errorf("failed to close old zip reader: %w", err)
}
}
// Reopen the zip file
zipReader, err := zip.OpenReader(p.zipPath)
if err != nil {
return fmt.Errorf("failed to reopen zip file: %w", err)
}
p.zipReader = zipReader
p.zipFS = zipfs.NewZipFS(&zipReader.Reader)
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
}