agent-orchestrator/backend/internal/preview/entry.go

97 lines
2.5 KiB
Go

package preview
import (
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
)
var entryCandidates = []string{"index.html", "public/index.html", "dist/index.html", "build/index.html"}
// Entry is a workspace-local static frontend entrypoint.
type Entry struct {
Path string
AbsPath string
ModTime time.Time
Size int64
}
// DiscoverEntry returns the first supported HTML entrypoint that exists inside
// the workspace.
func DiscoverEntry(workspacePath string) (Entry, bool) {
if strings.TrimSpace(workspacePath) == "" {
return Entry{}, false
}
for _, candidate := range entryCandidates {
file, ok := ConfinedPath(workspacePath, candidate)
if !ok {
continue
}
info, err := os.Stat(file)
if err == nil && !info.IsDir() {
return Entry{Path: candidate, AbsPath: file, ModTime: info.ModTime(), Size: info.Size()}, true
}
}
return Entry{}, false
}
// ConfinedPath maps an asset path into workspacePath and rejects paths that
// escape the workspace root.
func ConfinedPath(workspacePath, assetPath string) (string, bool) {
root, err := filepath.Abs(workspacePath)
if err != nil || root == "" {
return "", false
}
clean := strings.TrimPrefix(path.Clean("/"+strings.TrimSpace(assetPath)), "/")
if clean == "" || clean == "." {
clean = "index.html"
}
file := filepath.Join(root, filepath.FromSlash(clean))
absFile, err := filepath.Abs(file)
if err != nil {
return "", false
}
rel, err := filepath.Rel(root, absFile)
if err != nil || rel == "." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." {
return "", false
}
return absFile, true
}
// FileURL builds the daemon preview/files URL for a workspace-local entry.
func FileURL(baseURL string, id domain.SessionID, entry string) string {
u := normalizedBaseURL(baseURL)
u.Path = "/api/v1/sessions/" + url.PathEscape(string(id)) + "/preview/files/" + escapePath(entry)
u.RawQuery = ""
u.Fragment = ""
return u.String()
}
func normalizedBaseURL(raw string) url.URL {
raw = strings.TrimRight(strings.TrimSpace(raw), "/")
if raw == "" {
raw = "http://127.0.0.1:3001"
}
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return url.URL{Scheme: "http", Host: raw}
}
return *u
}
func escapePath(raw string) string {
parts := strings.Split(raw, "/")
for i, part := range parts {
parts[i] = url.PathEscape(part)
}
return strings.Join(parts, "/")
}