2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00
Files
panel/internal/data/template.go
2026-01-22 03:44:20 +08:00

184 lines
4.0 KiB
Go

package data
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/leonelquinteros/gotext"
"github.com/acepanel/panel/internal/app"
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/pkg/api"
"github.com/acepanel/panel/pkg/firewall"
"github.com/acepanel/panel/pkg/types"
)
type templateRepo struct {
t *gotext.Locale
cache biz.CacheRepo
api *api.API
firewall *firewall.Firewall
}
func NewTemplateRepo(t *gotext.Locale, cache biz.CacheRepo) biz.TemplateRepo {
return &templateRepo{
t: t,
cache: cache,
api: api.NewAPI(app.Version, app.Locale),
firewall: firewall.NewFirewall(),
}
}
// List 获取所有模版
func (r *templateRepo) List() api.Templates {
cached, err := r.cache.Get(biz.CacheKeyTemplates)
if err != nil {
return nil
}
templates := make(api.Templates, 0)
if err = json.Unmarshal([]byte(cached), &templates); err != nil {
return nil
}
return templates
}
// Get 获取模版详情
func (r *templateRepo) Get(slug string) (*api.Template, error) {
templates := r.List()
for _, t := range templates {
if t.Slug == slug {
return t, nil
}
}
return nil, errors.New(r.t.Get("template %s not found", slug))
}
// Callback 模版下载回调
func (r *templateRepo) Callback(slug string) error {
return r.api.TemplateCallback(slug)
}
// CreateCompose 创建编排
func (r *templateRepo) CreateCompose(name, compose string, envs []types.KV, autoFirewall bool) (string, error) {
dir := filepath.Join(app.Root, "compose", name)
// 检查编排是否已存在
if _, err := os.Stat(dir); err == nil {
return "", errors.New(r.t.Get("compose %s already exists", name))
}
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
if err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte(compose), 0644); err != nil {
return "", err
}
var sb strings.Builder
for _, kv := range envs {
sb.WriteString(kv.Key)
sb.WriteString("=")
sb.WriteString(kv.Value)
sb.WriteString("\n")
}
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte(sb.String()), 0644); err != nil {
return "", err
}
// 自动放行端口
if autoFirewall {
ports := r.parsePortsFromCompose(compose)
for _, port := range ports {
_ = r.firewall.Port(firewall.FireInfo{
Family: "ipv4",
PortStart: port.Port,
PortEnd: port.Port,
Protocol: port.Protocol,
Strategy: firewall.StrategyAccept,
Direction: "in",
}, firewall.OperationAdd)
}
}
return dir, nil
}
type composePort struct {
Port uint
Protocol firewall.Protocol
}
// parsePortsFromCompose 从 compose 文件中解析端口
func (r *templateRepo) parsePortsFromCompose(compose string) []composePort {
var ports []composePort
seen := make(map[string]bool)
// 匹配 ports 部分的端口映射
// 支持格式: "8080:80", "8080:80/tcp", "8080:80/udp", "80", "80/tcp"
portRegex := regexp.MustCompile(`(?m)^\s*-\s*["']?(\d+)(?::\d+)?(?:/(\w+))?["']?\s*$`)
matches := portRegex.FindAllStringSubmatch(compose, -1)
for _, match := range matches {
if len(match) < 2 {
continue
}
portStr := match[1]
protocol := firewall.ProtocolTCP
if len(match) > 2 && match[2] != "" {
switch strings.ToLower(match[2]) {
case "udp":
protocol = firewall.ProtocolUDP
case "tcp":
protocol = firewall.ProtocolTCP
}
}
// 去重
key := portStr + "/" + string(protocol)
if seen[key] {
continue
}
seen[key] = true
var port uint
if _, _, found := strings.Cut(portStr, ":"); found {
// 格式: host:container
parts := strings.Split(portStr, ":")
if len(parts) > 0 {
port = parseUint(parts[0])
}
} else {
port = parseUint(portStr)
}
if port > 0 && port <= 65535 {
ports = append(ports, composePort{
Port: port,
Protocol: protocol,
})
}
}
return ports
}
func parseUint(s string) uint {
var n uint
for _, c := range s {
if c >= '0' && c <= '9' {
n = n*10 + uint(c-'0')
} else {
break
}
}
return n
}