forked from aegis/pyserveX
264 lines
5.1 KiB
Go
264 lines
5.1 KiB
Go
package pathmatcher
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type MountedPath struct {
|
|
path string
|
|
name string
|
|
stripPath bool
|
|
}
|
|
|
|
func NewMountedPath(path string, opts ...MountedPathOption) *MountedPath {
|
|
// Normalize: remove trailing slash (except for root)
|
|
normalizedPath := strings.TrimSuffix(path, "/")
|
|
if normalizedPath == "" {
|
|
normalizedPath = ""
|
|
}
|
|
|
|
m := &MountedPath{
|
|
path: normalizedPath,
|
|
name: normalizedPath,
|
|
stripPath: true,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(m)
|
|
}
|
|
|
|
if m.name == "" {
|
|
m.name = normalizedPath
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
type MountedPathOption func(*MountedPath)
|
|
|
|
func WithName(name string) MountedPathOption {
|
|
return func(m *MountedPath) {
|
|
m.name = name
|
|
}
|
|
}
|
|
|
|
func WithStripPath(strip bool) MountedPathOption {
|
|
return func(m *MountedPath) {
|
|
m.stripPath = strip
|
|
}
|
|
}
|
|
|
|
func (m *MountedPath) Path() string {
|
|
return m.path
|
|
}
|
|
|
|
func (m *MountedPath) Name() string {
|
|
return m.name
|
|
}
|
|
|
|
func (m *MountedPath) StripPath() bool {
|
|
return m.stripPath
|
|
}
|
|
|
|
func (m *MountedPath) Matches(requestPath string) bool {
|
|
// Empty or "/" mount matches everything
|
|
if m.path == "" || m.path == "/" {
|
|
return true
|
|
}
|
|
|
|
// Request path must be at least as long as mount path
|
|
if len(requestPath) < len(m.path) {
|
|
return false
|
|
}
|
|
|
|
// Check if request path starts with mount path
|
|
if !strings.HasPrefix(requestPath, m.path) {
|
|
return false
|
|
}
|
|
|
|
// If paths are equal length, it's a match
|
|
if len(requestPath) == len(m.path) {
|
|
return true
|
|
}
|
|
|
|
// Otherwise, next char must be '/' to prevent /api matching /api-v2
|
|
return requestPath[len(m.path)] == '/'
|
|
}
|
|
|
|
func (m *MountedPath) GetModifiedPath(requestPath string) string {
|
|
if !m.stripPath {
|
|
return requestPath
|
|
}
|
|
|
|
// Root mount doesn't strip anything
|
|
if m.path == "" || m.path == "/" {
|
|
return requestPath
|
|
}
|
|
|
|
// Strip the prefix
|
|
modified := strings.TrimPrefix(requestPath, m.path)
|
|
|
|
// Ensure result starts with /
|
|
if modified == "" || modified[0] != '/' {
|
|
modified = "/" + modified
|
|
}
|
|
|
|
return modified
|
|
}
|
|
|
|
type MountManager struct {
|
|
mounts []*MountedPath
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewMountManager() *MountManager {
|
|
return &MountManager{
|
|
mounts: make([]*MountedPath, 0),
|
|
}
|
|
}
|
|
|
|
func (mm *MountManager) AddMount(mount *MountedPath) {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
|
|
// Insert in sorted order (longer paths first)
|
|
inserted := false
|
|
for i, existing := range mm.mounts {
|
|
if len(mount.path) > len(existing.path) {
|
|
// Insert at position i
|
|
mm.mounts = append(mm.mounts[:i], append([]*MountedPath{mount}, mm.mounts[i:]...)...)
|
|
inserted = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !inserted {
|
|
mm.mounts = append(mm.mounts, mount)
|
|
}
|
|
}
|
|
|
|
func (mm *MountManager) RemoveMount(path string) bool {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
|
|
normalizedPath := strings.TrimSuffix(path, "/")
|
|
|
|
for i, mount := range mm.mounts {
|
|
if mount.path == normalizedPath {
|
|
mm.mounts = append(mm.mounts[:i], mm.mounts[i+1:]...)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (mm *MountManager) GetMount(requestPath string) *MountedPath {
|
|
mm.mu.RLock()
|
|
defer mm.mu.RUnlock()
|
|
|
|
// Mounts are sorted by path length (longest first)
|
|
// so the first match is the best match
|
|
for _, mount := range mm.mounts {
|
|
if mount.Matches(requestPath) {
|
|
return mount
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mm *MountManager) MountCount() int {
|
|
mm.mu.RLock()
|
|
defer mm.mu.RUnlock()
|
|
return len(mm.mounts)
|
|
}
|
|
|
|
func (mm *MountManager) Mounts() []*MountedPath {
|
|
mm.mu.RLock()
|
|
defer mm.mu.RUnlock()
|
|
|
|
result := make([]*MountedPath, len(mm.mounts))
|
|
copy(result, mm.mounts)
|
|
return result
|
|
}
|
|
|
|
func (mm *MountManager) ListMounts() []map[string]interface{} {
|
|
mm.mu.RLock()
|
|
defer mm.mu.RUnlock()
|
|
|
|
result := make([]map[string]interface{}, len(mm.mounts))
|
|
for i, mount := range mm.mounts {
|
|
result[i] = map[string]interface{}{
|
|
"path": mount.path,
|
|
"name": mount.name,
|
|
"strip_path": mount.stripPath,
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Utility functions
|
|
|
|
func PathMatchesPrefix(requestPath, prefix string) bool {
|
|
// Normalize prefix
|
|
prefix = strings.TrimSuffix(prefix, "/")
|
|
|
|
// Empty or "/" prefix matches everything
|
|
if prefix == "" || prefix == "/" {
|
|
return true
|
|
}
|
|
|
|
// Request path must be at least as long as prefix
|
|
if len(requestPath) < len(prefix) {
|
|
return false
|
|
}
|
|
|
|
// Check if request path starts with prefix
|
|
if !strings.HasPrefix(requestPath, prefix) {
|
|
return false
|
|
}
|
|
|
|
// If paths are equal length, it's a match
|
|
if len(requestPath) == len(prefix) {
|
|
return true
|
|
}
|
|
|
|
// Otherwise, next char must be '/'
|
|
return requestPath[len(prefix)] == '/'
|
|
}
|
|
|
|
func StripPathPrefix(requestPath, prefix string) string {
|
|
// Normalize prefix
|
|
prefix = strings.TrimSuffix(prefix, "/")
|
|
|
|
// Empty or "/" prefix doesn't strip anything
|
|
if prefix == "" || prefix == "/" {
|
|
return requestPath
|
|
}
|
|
|
|
// Strip the prefix
|
|
modified := strings.TrimPrefix(requestPath, prefix)
|
|
|
|
// Ensure result starts with /
|
|
if modified == "" || modified[0] != '/' {
|
|
modified = "/" + modified
|
|
}
|
|
|
|
return modified
|
|
}
|
|
|
|
func MatchAndModifyPath(requestPath, prefix string, stripPath bool) (matches bool, modifiedPath string) {
|
|
if !PathMatchesPrefix(requestPath, prefix) {
|
|
return false, ""
|
|
}
|
|
|
|
if stripPath {
|
|
return true, StripPathPrefix(requestPath, prefix)
|
|
}
|
|
|
|
return true, requestPath
|
|
}
|