mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-09 13:04:24 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70bf0a4be1 | |||
| 4964d89158 | |||
| 96b098f912 | |||
| 5bba99efe3 | |||
| 8504b6d13d | |||
| ada4db6465 | |||
| 2017465cb8 | |||
| d33747c2d3 |
@@ -3,6 +3,7 @@ package dbmanager
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -159,6 +160,9 @@ func (c *sqlConnection) Close() error {
|
|||||||
|
|
||||||
// HealthCheck verifies the connection is alive
|
// HealthCheck verifies the connection is alive
|
||||||
func (c *sqlConnection) HealthCheck(ctx context.Context) error {
|
func (c *sqlConnection) HealthCheck(ctx context.Context) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@@ -188,6 +192,9 @@ func (c *sqlConnection) Reconnect(ctx context.Context) error {
|
|||||||
|
|
||||||
// Native returns the native *sql.DB connection
|
// Native returns the native *sql.DB connection
|
||||||
func (c *sqlConnection) Native() (*sql.DB, error) {
|
func (c *sqlConnection) Native() (*sql.DB, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if c.nativeDB != nil {
|
if c.nativeDB != nil {
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -219,6 +226,9 @@ func (c *sqlConnection) Native() (*sql.DB, error) {
|
|||||||
|
|
||||||
// Bun returns a Bun ORM instance wrapping the native connection
|
// Bun returns a Bun ORM instance wrapping the native connection
|
||||||
func (c *sqlConnection) Bun() (*bun.DB, error) {
|
func (c *sqlConnection) Bun() (*bun.DB, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if c.bunDB != nil {
|
if c.bunDB != nil {
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -249,6 +259,9 @@ func (c *sqlConnection) Bun() (*bun.DB, error) {
|
|||||||
|
|
||||||
// GORM returns a GORM instance wrapping the native connection
|
// GORM returns a GORM instance wrapping the native connection
|
||||||
func (c *sqlConnection) GORM() (*gorm.DB, error) {
|
func (c *sqlConnection) GORM() (*gorm.DB, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if c.gormDB != nil {
|
if c.gormDB != nil {
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -283,6 +296,9 @@ func (c *sqlConnection) GORM() (*gorm.DB, error) {
|
|||||||
|
|
||||||
// Database returns the common.Database interface using the configured default ORM
|
// Database returns the common.Database interface using the configured default ORM
|
||||||
func (c *sqlConnection) Database() (common.Database, error) {
|
func (c *sqlConnection) Database() (common.Database, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defaultORM := c.config.DefaultORM
|
defaultORM := c.config.DefaultORM
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
@@ -307,6 +323,9 @@ func (c *sqlConnection) MongoDB() (*mongo.Client, error) {
|
|||||||
|
|
||||||
// Stats returns connection statistics
|
// Stats returns connection statistics
|
||||||
func (c *sqlConnection) Stats() *ConnectionStats {
|
func (c *sqlConnection) Stats() *ConnectionStats {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
@@ -336,6 +355,9 @@ func (c *sqlConnection) Stats() *ConnectionStats {
|
|||||||
|
|
||||||
// getBunAdapter returns or creates the Bun adapter
|
// getBunAdapter returns or creates the Bun adapter
|
||||||
func (c *sqlConnection) getBunAdapter() (common.Database, error) {
|
func (c *sqlConnection) getBunAdapter() (common.Database, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if c.bunAdapter != nil {
|
if c.bunAdapter != nil {
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -361,6 +383,9 @@ func (c *sqlConnection) getBunAdapter() (common.Database, error) {
|
|||||||
|
|
||||||
// getGORMAdapter returns or creates the GORM adapter
|
// getGORMAdapter returns or creates the GORM adapter
|
||||||
func (c *sqlConnection) getGORMAdapter() (common.Database, error) {
|
func (c *sqlConnection) getGORMAdapter() (common.Database, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if c.gormAdapter != nil {
|
if c.gormAdapter != nil {
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -386,6 +411,9 @@ func (c *sqlConnection) getGORMAdapter() (common.Database, error) {
|
|||||||
|
|
||||||
// getNativeAdapter returns or creates the native adapter
|
// getNativeAdapter returns or creates the native adapter
|
||||||
func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
|
func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if c.nativeAdapter != nil {
|
if c.nativeAdapter != nil {
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -424,6 +452,7 @@ func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
|
|||||||
|
|
||||||
// getBunDialect returns the appropriate Bun dialect for the database type
|
// getBunDialect returns the appropriate Bun dialect for the database type
|
||||||
func (c *sqlConnection) getBunDialect() schema.Dialect {
|
func (c *sqlConnection) getBunDialect() schema.Dialect {
|
||||||
|
|
||||||
switch c.dbType {
|
switch c.dbType {
|
||||||
case DatabaseTypePostgreSQL:
|
case DatabaseTypePostgreSQL:
|
||||||
return database.GetPostgresDialect()
|
return database.GetPostgresDialect()
|
||||||
|
|||||||
@@ -207,9 +207,14 @@ func ExampleWithBun(bunDB *bun.DB) {
|
|||||||
SetupMuxRoutes(muxRouter, handler, nil)
|
SetupMuxRoutes(muxRouter, handler, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BunRouterHandler is an interface that both bunrouter.Router and bunrouter.Group implement
|
||||||
|
type BunRouterHandler interface {
|
||||||
|
Handle(method, path string, handler bunrouter.HandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
// SetupBunRouterRoutes sets up bunrouter routes for the ResolveSpec API
|
// SetupBunRouterRoutes sets up bunrouter routes for the ResolveSpec API
|
||||||
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
|
// Accepts bunrouter.Router or bunrouter.Group
|
||||||
r := bunRouter.GetBunRouter()
|
func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
|
||||||
|
|
||||||
// CORS config
|
// CORS config
|
||||||
corsConfig := common.DefaultCORSConfig()
|
corsConfig := common.DefaultCORSConfig()
|
||||||
@@ -337,13 +342,13 @@ func ExampleWithBunRouter(bunDB *bun.DB) {
|
|||||||
handler := NewHandlerWithBun(bunDB)
|
handler := NewHandlerWithBun(bunDB)
|
||||||
|
|
||||||
// Create bunrouter
|
// Create bunrouter
|
||||||
bunRouter := router.NewStandardBunRouterAdapter()
|
bunRouter := bunrouter.New()
|
||||||
|
|
||||||
// Setup ResolveSpec routes with bunrouter
|
// Setup ResolveSpec routes with bunrouter
|
||||||
SetupBunRouterRoutes(bunRouter, handler)
|
SetupBunRouterRoutes(bunRouter, handler)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
// http.ListenAndServe(":8080", bunRouter.GetBunRouter())
|
// http.ListenAndServe(":8080", bunRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExampleBunRouterWithBunDB shows the full uptrace stack (bunrouter + Bun ORM)
|
// ExampleBunRouterWithBunDB shows the full uptrace stack (bunrouter + Bun ORM)
|
||||||
@@ -359,11 +364,29 @@ func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
|
|||||||
handler := NewHandler(dbAdapter, registry)
|
handler := NewHandler(dbAdapter, registry)
|
||||||
|
|
||||||
// Create bunrouter
|
// Create bunrouter
|
||||||
bunRouter := router.NewStandardBunRouterAdapter()
|
bunRouter := bunrouter.New()
|
||||||
|
|
||||||
// Setup ResolveSpec routes
|
// Setup ResolveSpec routes
|
||||||
SetupBunRouterRoutes(bunRouter, handler)
|
SetupBunRouterRoutes(bunRouter, handler)
|
||||||
|
|
||||||
// This gives you the full uptrace stack: bunrouter + Bun ORM
|
// This gives you the full uptrace stack: bunrouter + Bun ORM
|
||||||
// http.ListenAndServe(":8080", bunRouter.GetBunRouter())
|
// http.ListenAndServe(":8080", bunRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleBunRouterWithGroup shows how to use SetupBunRouterRoutes with a bunrouter.Group
|
||||||
|
func ExampleBunRouterWithGroup(bunDB *bun.DB) {
|
||||||
|
// Create handler with Bun adapter
|
||||||
|
handler := NewHandlerWithBun(bunDB)
|
||||||
|
|
||||||
|
// Create bunrouter
|
||||||
|
bunRouter := bunrouter.New()
|
||||||
|
|
||||||
|
// Create a route group with a prefix
|
||||||
|
apiGroup := bunRouter.NewGroup("/api")
|
||||||
|
|
||||||
|
// Setup ResolveSpec routes on the group - routes will be under /api
|
||||||
|
SetupBunRouterRoutes(apiGroup, handler)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
// http.ListenAndServe(":8080", bunRouter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,9 +270,14 @@ func ExampleWithBun(bunDB *bun.DB) {
|
|||||||
SetupMuxRoutes(muxRouter, handler, nil)
|
SetupMuxRoutes(muxRouter, handler, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BunRouterHandler is an interface that both bunrouter.Router and bunrouter.Group implement
|
||||||
|
type BunRouterHandler interface {
|
||||||
|
Handle(method, path string, handler bunrouter.HandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
|
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
|
||||||
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
|
// Accepts bunrouter.Router or bunrouter.Group
|
||||||
r := bunRouter.GetBunRouter()
|
func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
|
||||||
|
|
||||||
// CORS config
|
// CORS config
|
||||||
corsConfig := common.DefaultCORSConfig()
|
corsConfig := common.DefaultCORSConfig()
|
||||||
@@ -450,17 +455,34 @@ func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
|
|||||||
// Create handler
|
// Create handler
|
||||||
handler := NewHandlerWithBun(bunDB)
|
handler := NewHandlerWithBun(bunDB)
|
||||||
|
|
||||||
// Create BunRouter adapter
|
// Create bunrouter
|
||||||
routerAdapter := NewStandardBunRouter()
|
bunRouter := bunrouter.New()
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
SetupBunRouterRoutes(routerAdapter, handler)
|
SetupBunRouterRoutes(bunRouter, handler)
|
||||||
|
|
||||||
// Get the underlying router for server setup
|
|
||||||
r := routerAdapter.GetBunRouter()
|
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
if err := http.ListenAndServe(":8080", r); err != nil {
|
if err := http.ListenAndServe(":8080", bunRouter); err != nil {
|
||||||
|
logger.Error("Server failed to start: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleBunRouterWithGroup shows how to use SetupBunRouterRoutes with a bunrouter.Group
|
||||||
|
func ExampleBunRouterWithGroup(bunDB *bun.DB) {
|
||||||
|
// Create handler with Bun adapter
|
||||||
|
handler := NewHandlerWithBun(bunDB)
|
||||||
|
|
||||||
|
// Create bunrouter
|
||||||
|
bunRouter := bunrouter.New()
|
||||||
|
|
||||||
|
// Create a route group with a prefix
|
||||||
|
apiGroup := bunRouter.NewGroup("/api")
|
||||||
|
|
||||||
|
// Setup RestHeadSpec routes on the group - routes will be under /api
|
||||||
|
SetupBunRouterRoutes(apiGroup, handler)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
if err := http.ListenAndServe(":8080", bunRouter); err != nil {
|
||||||
logger.Error("Server failed to start: %v", err)
|
logger.Error("Server failed to start: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package staticweb
|
|||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileSystemProvider abstracts the source of files (local, zip, embedded, future: http, s3)
|
// FileSystemProvider abstracts the source of files (local, zip, embedded, future: http, s3)
|
||||||
@@ -34,6 +35,32 @@ 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 provider == nil || reflect.ValueOf(provider).IsNil() {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
if p, ok := provider.(PrefixStrippingProvider); ok && p != nil {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/server/zipfs"
|
"github.com/bitechdev/ResolveSpec/pkg/server/zipfs"
|
||||||
@@ -15,16 +17,18 @@ import (
|
|||||||
// EmbedFSProvider serves files from an embedded filesystem.
|
// EmbedFSProvider serves files from an embedded filesystem.
|
||||||
// It supports both direct embedded directories and embedded zip files.
|
// It supports both direct embedded directories and embedded zip files.
|
||||||
type EmbedFSProvider struct {
|
type EmbedFSProvider struct {
|
||||||
embedFS *embed.FS
|
embedFS *embed.FS
|
||||||
zipFile string // Optional: path within embedded FS to zip file
|
zipFile string // Optional: path within embedded FS to zip file
|
||||||
zipReader *zip.Reader
|
stripPrefix string // Optional: prefix to strip from requested paths (e.g., "/dist")
|
||||||
fs fs.FS
|
zipReader *zip.Reader
|
||||||
mu sync.RWMutex
|
fs fs.FS
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEmbedFSProvider creates a new EmbedFSProvider.
|
// NewEmbedFSProvider creates a new EmbedFSProvider.
|
||||||
// If zipFile is empty, the embedded FS is used directly.
|
// If zipFile is empty, the embedded FS is used directly.
|
||||||
// If zipFile is specified, it's treated as a path to a zip file within the embedded FS.
|
// If zipFile is specified, it's treated as a path to a zip file within the embedded FS.
|
||||||
|
// Use WithStripPrefix to configure path prefix stripping.
|
||||||
func NewEmbedFSProvider(embedFS fs.FS, zipFile string) (*EmbedFSProvider, error) {
|
func NewEmbedFSProvider(embedFS fs.FS, zipFile string) (*EmbedFSProvider, error) {
|
||||||
if embedFS == nil {
|
if embedFS == nil {
|
||||||
return nil, fmt.Errorf("embedded filesystem cannot be nil")
|
return nil, fmt.Errorf("embedded filesystem cannot be nil")
|
||||||
@@ -81,6 +85,9 @@ func NewEmbedFSProvider(embedFS fs.FS, zipFile string) (*EmbedFSProvider, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open opens the named file from the embedded filesystem.
|
// Open opens the named file from the embedded filesystem.
|
||||||
|
// 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 embedded filesystem.
|
||||||
func (p *EmbedFSProvider) Open(name string) (fs.File, error) {
|
func (p *EmbedFSProvider) Open(name string) (fs.File, error) {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
@@ -89,7 +96,21 @@ func (p *EmbedFSProvider) Open(name string) (fs.File, error) {
|
|||||||
return nil, fmt.Errorf("embedded filesystem is closed")
|
return nil, fmt.Errorf("embedded filesystem is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.fs.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.fs.Open(actualPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close releases any resources held by the provider.
|
// Close releases any resources held by the provider.
|
||||||
@@ -117,3 +138,19 @@ func (p *EmbedFSProvider) Type() string {
|
|||||||
func (p *EmbedFSProvider) ZipFile() string {
|
func (p *EmbedFSProvider) ZipFile() string {
|
||||||
return p.zipFile
|
return p.zipFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithStripPrefix sets the prefix to strip from requested paths.
|
||||||
|
// For example, WithStripPrefix("/dist") will make files at "/dist/assets"
|
||||||
|
// accessible via "/assets".
|
||||||
|
func (p *EmbedFSProvider) WithStripPrefix(prefix string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.stripPrefix = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripPrefix returns the configured strip prefix.
|
||||||
|
func (p *EmbedFSProvider) StripPrefix() string {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
return p.stripPrefix
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user