mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 05:31:44 +08:00
feat: 项目管理阶段1
This commit is contained in:
@@ -83,6 +83,8 @@ func initWeb() (*app.Web, error) {
|
||||
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, appRepo, environmentRepo, settingRepo, cronRepo, backupRepo)
|
||||
taskService := service.NewTaskService(taskRepo)
|
||||
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
|
||||
projectRepo := data.NewProjectRepo(locale, db)
|
||||
projectService := service.NewProjectService(projectRepo)
|
||||
databaseService := service.NewDatabaseService(databaseRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
@@ -143,7 +145,7 @@ func initWeb() (*app.Web, error) {
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, webHookService, loader)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, webHookService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
|
||||
1
go.mod
1
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/bddjr/hlfhr v1.4.0
|
||||
github.com/beevik/ntp v1.5.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/coreos/go-systemd/v22 v22.6.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/expr-lang/expr v1.17.7
|
||||
|
||||
2
go.sum
2
go.sum
@@ -50,6 +50,8 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
|
||||
25
internal/biz/project.go
Normal file
25
internal/biz/project.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"` // 项目名称
|
||||
Type types.ProjectType `gorm:"not null;index;default:'general'" json:"type"` // 项目类型
|
||||
Path string `gorm:"not null;default:''" json:"path"` // 项目路径
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProjectRepo interface {
|
||||
List(page, limit uint) ([]*types.ProjectDetail, int64, error)
|
||||
Get(id uint) (*types.ProjectDetail, error)
|
||||
Create(req *request.ProjectCreate) (*types.ProjectDetail, error)
|
||||
Update(req *request.ProjectUpdate) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
type Website struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:'';unique" json:"name"`
|
||||
Type WebsiteType `gorm:"not null;default:'static'" json:"type"`
|
||||
Type WebsiteType `gorm:"not null;index;default:'static'" json:"type"`
|
||||
Status bool `gorm:"not null;default:true" json:"status"`
|
||||
Path string `gorm:"not null;default:''" json:"path"`
|
||||
SSL bool `gorm:"not null;default:false" json:"ssl"`
|
||||
|
||||
@@ -21,6 +21,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewDatabaseUserRepo,
|
||||
NewEnvironmentRepo,
|
||||
NewMonitorRepo,
|
||||
NewProjectRepo,
|
||||
NewSafeRepo,
|
||||
NewSettingRepo,
|
||||
NewSSHRepo,
|
||||
|
||||
487
internal/data/project.go
Normal file
487
internal/data/project.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/unit"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
)
|
||||
|
||||
type projectRepo struct {
|
||||
systemdDir string
|
||||
t *gotext.Locale
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProjectRepo(t *gotext.Locale, db *gorm.DB) biz.ProjectRepo {
|
||||
return &projectRepo{
|
||||
systemdDir: "/etc/systemd/system",
|
||||
t: t,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *projectRepo) List(page, limit uint) ([]*types.ProjectDetail, int64, error) {
|
||||
var projects []*biz.Project
|
||||
var total int64
|
||||
|
||||
if err := r.db.Model(&biz.Project{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := r.db.Offset(int((page - 1) * limit)).Limit(int(limit)).Order("id desc").Find(&projects).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
details := make([]*types.ProjectDetail, 0, len(projects))
|
||||
for _, p := range projects {
|
||||
detail, err := r.parseProjectDetail(p)
|
||||
if err != nil {
|
||||
// 如果解析失败,返回基本信息
|
||||
detail = &types.ProjectDetail{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
}
|
||||
}
|
||||
details = append(details, detail)
|
||||
}
|
||||
|
||||
return details, total, nil
|
||||
}
|
||||
|
||||
func (r *projectRepo) Get(id uint) (*types.ProjectDetail, error) {
|
||||
project := new(biz.Project)
|
||||
if err := r.db.First(project, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.parseProjectDetail(project)
|
||||
}
|
||||
|
||||
func (r *projectRepo) Create(req *request.ProjectCreate) (*types.ProjectDetail, error) {
|
||||
// 检查项目名是否已存在
|
||||
var count int64
|
||||
if err := r.db.Model(&biz.Project{}).Where("name = ?", req.Name).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New(r.t.Get("project name already exists"))
|
||||
}
|
||||
|
||||
project := &biz.Project{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Path: req.RootDir,
|
||||
}
|
||||
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 创建数据库记录
|
||||
if err := tx.Create(project).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成 systemd unit 文件
|
||||
if err := r.generateUnitFile(project.ID, req); err != nil {
|
||||
return fmt.Errorf("%s: %w", r.t.Get("failed to generate systemd config"), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.parseProjectDetail(project)
|
||||
}
|
||||
|
||||
func (r *projectRepo) Update(req *request.ProjectUpdate) error {
|
||||
project := new(biz.Project)
|
||||
if err := r.db.First(project, req.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果名称变更,需要重命名 unit 文件
|
||||
if req.Name != project.Name {
|
||||
oldPath := r.unitFilePath(project.Name)
|
||||
newPath := r.unitFilePath(req.Name)
|
||||
if err := os.Rename(oldPath, newPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("%s: %w", r.t.Get("failed to rename systemd config"), err)
|
||||
}
|
||||
project.Name = req.Name
|
||||
}
|
||||
|
||||
project.Path = req.RootDir
|
||||
if err := r.db.Save(project).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新 systemd unit 文件
|
||||
return r.updateUnitFile(project.Name, req)
|
||||
}
|
||||
|
||||
func (r *projectRepo) Delete(id uint) error {
|
||||
project := new(biz.Project)
|
||||
if err := r.db.First(project, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除 systemd unit 文件
|
||||
unitPath := r.unitFilePath(project.Name)
|
||||
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("%s: %w", r.t.Get("failed to delete systemd config"), err)
|
||||
}
|
||||
|
||||
return r.db.Delete(project).Error
|
||||
}
|
||||
|
||||
// unitFilePath 返回 systemd unit 文件路径
|
||||
func (r *projectRepo) unitFilePath(name string) string {
|
||||
return filepath.Join(r.systemdDir, fmt.Sprintf("acepanel-project-%s.service", name))
|
||||
}
|
||||
|
||||
// parseProjectDetail 从数据库记录和 systemd unit 文件解析项目详情
|
||||
func (r *projectRepo) parseProjectDetail(project *biz.Project) (*types.ProjectDetail, error) {
|
||||
detail := &types.ProjectDetail{
|
||||
ID: project.ID,
|
||||
Name: project.Name,
|
||||
Type: project.Type,
|
||||
RootDir: project.Path,
|
||||
}
|
||||
|
||||
// 读取并解析 systemd unit 文件
|
||||
unitPath := r.unitFilePath(project.Name)
|
||||
file, err := os.Open(unitPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return detail, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
options, err := unit.DeserializeOptions(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", r.t.Get("failed to parse systemd config"), err)
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
for _, opt := range options {
|
||||
switch opt.Section {
|
||||
case "Unit":
|
||||
r.parseUnitSection(detail, opt)
|
||||
case "Service":
|
||||
r.parseServiceSection(detail, opt)
|
||||
}
|
||||
}
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
// parseUnitSection 解析 [Unit] 部分
|
||||
func (r *projectRepo) parseUnitSection(detail *types.ProjectDetail, opt *unit.UnitOption) {
|
||||
switch opt.Name {
|
||||
case "Description":
|
||||
detail.Description = opt.Value
|
||||
case "Requires":
|
||||
detail.Requires = append(detail.Requires, opt.Value)
|
||||
case "Wants":
|
||||
detail.Wants = append(detail.Wants, opt.Value)
|
||||
case "After":
|
||||
detail.After = append(detail.After, opt.Value)
|
||||
case "Before":
|
||||
detail.Before = append(detail.Before, opt.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// parseServiceSection 解析 [Service] 部分
|
||||
func (r *projectRepo) parseServiceSection(detail *types.ProjectDetail, opt *unit.UnitOption) {
|
||||
switch opt.Name {
|
||||
case "WorkingDirectory":
|
||||
detail.WorkingDir = opt.Value
|
||||
case "ExecStartPre":
|
||||
detail.ExecStartPre = opt.Value
|
||||
case "ExecStartPost":
|
||||
detail.ExecStartPost = opt.Value
|
||||
case "ExecStart":
|
||||
detail.ExecStart = opt.Value
|
||||
case "ExecStop":
|
||||
detail.ExecStop = opt.Value
|
||||
case "ExecReload":
|
||||
detail.ExecReload = opt.Value
|
||||
case "User":
|
||||
detail.User = opt.Value
|
||||
case "Restart":
|
||||
detail.Restart = opt.Value
|
||||
case "RestartSec":
|
||||
detail.RestartSec = opt.Value
|
||||
case "StartLimitBurst":
|
||||
if v, err := strconv.Atoi(opt.Value); err == nil {
|
||||
detail.RestartMax = v
|
||||
}
|
||||
case "TimeoutStartSec":
|
||||
if v, err := strconv.Atoi(opt.Value); err == nil {
|
||||
detail.TimeoutStartSec = v
|
||||
}
|
||||
case "TimeoutStopSec":
|
||||
if v, err := strconv.Atoi(opt.Value); err == nil {
|
||||
detail.TimeoutStopSec = v
|
||||
}
|
||||
case "Environment":
|
||||
// 格式: KEY=VALUE
|
||||
if kv := r.parseEnvironment(opt.Value); kv != nil {
|
||||
detail.Environments = append(detail.Environments, *kv)
|
||||
}
|
||||
case "StandardOutput":
|
||||
detail.StandardOutput = opt.Value
|
||||
case "StandardError":
|
||||
detail.StandardError = opt.Value
|
||||
case "MemoryLimit":
|
||||
if v, err := r.parseBytes(opt.Value); err == nil {
|
||||
detail.MemoryLimit = v
|
||||
}
|
||||
case "CPUQuota":
|
||||
if v, err := r.parsePercent(opt.Value); err == nil {
|
||||
detail.CPUQuota = v
|
||||
}
|
||||
case "NoNewPrivileges":
|
||||
detail.NoNewPrivileges = opt.Value == "true" || opt.Value == "yes"
|
||||
case "ProtectTmp":
|
||||
detail.ProtectTmp = opt.Value == "true" || opt.Value == "yes"
|
||||
case "ProtectHome":
|
||||
detail.ProtectHome = opt.Value == "true" || opt.Value == "yes"
|
||||
case "ProtectSystem":
|
||||
detail.ProtectSystem = opt.Value
|
||||
case "ReadWritePaths":
|
||||
detail.ReadWritePaths = append(detail.ReadWritePaths, opt.Value)
|
||||
case "ReadOnlyPaths":
|
||||
detail.ReadOnlyPaths = append(detail.ReadOnlyPaths, opt.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// parseEnvironment 解析环境变量
|
||||
func (r *projectRepo) parseEnvironment(value string) *types.KV {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil
|
||||
}
|
||||
return &types.KV{Key: parts[0], Value: parts[1]}
|
||||
}
|
||||
|
||||
// parseBytes 解析字节大小 (如 512M, 1G)
|
||||
func (r *projectRepo) parseBytes(value string) (float64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, errors.New("empty value")
|
||||
}
|
||||
|
||||
multiplier := float64(1)
|
||||
suffix := value[len(value)-1]
|
||||
switch suffix {
|
||||
case 'K', 'k':
|
||||
multiplier = 1024
|
||||
value = value[:len(value)-1]
|
||||
case 'M', 'm':
|
||||
multiplier = 1024 * 1024
|
||||
value = value[:len(value)-1]
|
||||
case 'G', 'g':
|
||||
multiplier = 1024 * 1024 * 1024
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
|
||||
return cast.ToFloat64(value) * multiplier, nil
|
||||
}
|
||||
|
||||
// formatBytes 格式化字节大小
|
||||
func (r *projectRepo) formatBytes(bytes float64) string {
|
||||
b := int64(bytes)
|
||||
if b >= 1024*1024*1024 && b%(1024*1024*1024) == 0 {
|
||||
return fmt.Sprintf("%dG", b/(1024*1024*1024))
|
||||
}
|
||||
if b >= 1024*1024 && b%(1024*1024) == 0 {
|
||||
return fmt.Sprintf("%dM", b/(1024*1024))
|
||||
}
|
||||
if b >= 1024 && b%1024 == 0 {
|
||||
return fmt.Sprintf("%dK", b/1024)
|
||||
}
|
||||
return strconv.FormatInt(b, 10)
|
||||
}
|
||||
|
||||
// parsePercent 解析百分比 (如 50%)
|
||||
func (r *projectRepo) parsePercent(value string) (float64, error) {
|
||||
value = strings.TrimSuffix(value, "%")
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
|
||||
// generateUnitFile 生成 systemd unit 文件
|
||||
func (r *projectRepo) generateUnitFile(id uint, req *request.ProjectCreate) error {
|
||||
options := []*unit.UnitOption{
|
||||
// [Unit] section
|
||||
unit.NewUnitOption("Unit", "Description", fmt.Sprintf("AcePanel Project: %s", req.Name)),
|
||||
unit.NewUnitOption("Unit", "After", "network.target"),
|
||||
|
||||
// [Service] section
|
||||
unit.NewUnitOption("Service", "Type", "simple"),
|
||||
unit.NewUnitOption("Service", "WorkingDirectory", lo.If(req.WorkingDir != "", req.WorkingDir).Else(req.RootDir)),
|
||||
}
|
||||
|
||||
if req.ExecStart != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecStart", req.ExecStart))
|
||||
}
|
||||
if req.User != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "User", req.User))
|
||||
}
|
||||
if req.Restart != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "Restart", req.Restart))
|
||||
} else {
|
||||
options = append(options, unit.NewUnitOption("Service", "Restart", "on-failure"))
|
||||
}
|
||||
|
||||
// 环境变量
|
||||
for _, env := range req.Environments {
|
||||
options = append(options, unit.NewUnitOption("Service", "Environment", fmt.Sprintf("%s=%s", env.Key, env.Value)))
|
||||
}
|
||||
|
||||
// [Install] section
|
||||
options = append(options, unit.NewUnitOption("Install", "WantedBy", "multi-user.target"))
|
||||
|
||||
// 写入文件
|
||||
unitPath := r.unitFilePath(req.Name)
|
||||
reader := unit.Serialize(options)
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(unitPath, content, 0644)
|
||||
}
|
||||
|
||||
// updateUnitFile 更新 systemd unit 文件
|
||||
func (r *projectRepo) updateUnitFile(name string, req *request.ProjectUpdate) error {
|
||||
options := []*unit.UnitOption{
|
||||
// [Unit] section
|
||||
unit.NewUnitOption("Unit", "Description", req.Description),
|
||||
}
|
||||
|
||||
// Unit 依赖
|
||||
for _, v := range req.Requires {
|
||||
options = append(options, unit.NewUnitOption("Unit", "Requires", v))
|
||||
}
|
||||
for _, v := range req.Wants {
|
||||
options = append(options, unit.NewUnitOption("Unit", "Wants", v))
|
||||
}
|
||||
for _, v := range req.After {
|
||||
options = append(options, unit.NewUnitOption("Unit", "After", v))
|
||||
}
|
||||
if len(req.After) == 0 {
|
||||
options = append(options, unit.NewUnitOption("Unit", "After", "network.target"))
|
||||
}
|
||||
for _, v := range req.Before {
|
||||
options = append(options, unit.NewUnitOption("Unit", "Before", v))
|
||||
}
|
||||
|
||||
// [Service] section
|
||||
options = append(options, unit.NewUnitOption("Service", "Type", "simple"))
|
||||
options = append(options, unit.NewUnitOption("Service", "WorkingDirectory", lo.If(req.WorkingDir != "", req.WorkingDir).Else(req.RootDir)))
|
||||
|
||||
if req.ExecStartPre != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecStartPre", req.ExecStartPre))
|
||||
}
|
||||
if req.ExecStart != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecStart", req.ExecStart))
|
||||
}
|
||||
if req.ExecStartPost != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecStartPost", req.ExecStartPost))
|
||||
}
|
||||
if req.ExecStop != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecStop", req.ExecStop))
|
||||
}
|
||||
if req.ExecReload != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecReload", req.ExecReload))
|
||||
}
|
||||
if req.User != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "User", req.User))
|
||||
}
|
||||
if req.Restart != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "Restart", req.Restart))
|
||||
} else {
|
||||
options = append(options, unit.NewUnitOption("Service", "Restart", "on-failure"))
|
||||
}
|
||||
if req.RestartSec != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "RestartSec", req.RestartSec))
|
||||
}
|
||||
if req.RestartMax > 0 {
|
||||
options = append(options, unit.NewUnitOption("Service", "StartLimitBurst", strconv.Itoa(req.RestartMax)))
|
||||
}
|
||||
if req.TimeoutStartSec > 0 {
|
||||
options = append(options, unit.NewUnitOption("Service", "TimeoutStartSec", strconv.Itoa(req.TimeoutStartSec)))
|
||||
}
|
||||
if req.TimeoutStopSec > 0 {
|
||||
options = append(options, unit.NewUnitOption("Service", "TimeoutStopSec", strconv.Itoa(req.TimeoutStopSec)))
|
||||
}
|
||||
|
||||
// 环境变量
|
||||
for _, env := range req.Environments {
|
||||
options = append(options, unit.NewUnitOption("Service", "Environment", fmt.Sprintf("%s=%s", env.Key, env.Value)))
|
||||
}
|
||||
|
||||
// 输出
|
||||
if req.StandardOutput != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "StandardOutput", req.StandardOutput))
|
||||
}
|
||||
if req.StandardError != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "StandardError", req.StandardError))
|
||||
}
|
||||
|
||||
// 资源限制
|
||||
if req.MemoryLimit > 0 {
|
||||
options = append(options, unit.NewUnitOption("Service", "MemoryLimit", r.formatBytes(req.MemoryLimit)))
|
||||
}
|
||||
if req.CPUQuota != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "CPUQuota", req.CPUQuota))
|
||||
}
|
||||
|
||||
// 安全选项
|
||||
if req.NoNewPrivileges {
|
||||
options = append(options, unit.NewUnitOption("Service", "NoNewPrivileges", "true"))
|
||||
}
|
||||
if req.ProtectTmp {
|
||||
options = append(options, unit.NewUnitOption("Service", "ProtectTmp", "true"))
|
||||
}
|
||||
if req.ProtectHome {
|
||||
options = append(options, unit.NewUnitOption("Service", "ProtectHome", "true"))
|
||||
}
|
||||
if req.ProtectSystem != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ProtectSystem", req.ProtectSystem))
|
||||
}
|
||||
for _, v := range req.ReadWritePaths {
|
||||
options = append(options, unit.NewUnitOption("Service", "ReadWritePaths", v))
|
||||
}
|
||||
for _, v := range req.ReadOnlyPaths {
|
||||
options = append(options, unit.NewUnitOption("Service", "ReadOnlyPaths", v))
|
||||
}
|
||||
|
||||
// [Install] section
|
||||
options = append(options, unit.NewUnitOption("Install", "WantedBy", "multi-user.target"))
|
||||
|
||||
// 写入文件
|
||||
unitPath := r.unitFilePath(name)
|
||||
reader := unit.Serialize(options)
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(unitPath, content, 0644)
|
||||
}
|
||||
51
internal/http/request/project.go
Normal file
51
internal/http/request/project.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package request
|
||||
|
||||
import "github.com/acepanel/panel/pkg/types"
|
||||
|
||||
type ProjectCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Type types.ProjectType `form:"type" json:"type" validate:"required|in:general,php,java,go,python,nodejs"`
|
||||
Description string `form:"description" json:"description"`
|
||||
RootDir string `form:"root_dir" json:"root_dir" validate:"required"`
|
||||
WorkingDir string `form:"working_dir" json:"working_dir"`
|
||||
ExecStart string `form:"exec_start" json:"exec_start"`
|
||||
User string `form:"user" json:"user"`
|
||||
Restart string `json:"restart"`
|
||||
Environments []types.KV
|
||||
}
|
||||
|
||||
type ProjectUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:projects,id"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Description string `form:"description" json:"description"`
|
||||
RootDir string `form:"root_dir" json:"root_dir" validate:"required"`
|
||||
WorkingDir string `form:"working_dir" json:"working_dir"`
|
||||
ExecStartPre string `form:"exec_start_pre" json:"exec_start_pre"`
|
||||
ExecStartPost string `form:"exec_start_post" json:"exec_start_post"`
|
||||
ExecStart string `form:"exec_start" json:"exec_start"`
|
||||
ExecStop string `form:"exec_stop" json:"exec_stop"`
|
||||
ExecReload string `form:"exec_reload" json:"exec_reload"`
|
||||
User string `form:"user" json:"user"`
|
||||
Restart string `json:"restart"`
|
||||
RestartSec string `json:"restart_sec"`
|
||||
RestartMax int `json:"restart_max"`
|
||||
TimeoutStartSec int `json:"timeout_start_sec"`
|
||||
TimeoutStopSec int `json:"timeout_stop_sec"`
|
||||
Environments []types.KV `form:"environments" json:"environments"`
|
||||
StandardOutput string `form:"standard_output" json:"standard_output"`
|
||||
StandardError string `form:"standard_error" json:"standard_error"`
|
||||
Requires []string `form:"requires" json:"requires"`
|
||||
Wants []string `form:"wants" json:"wants"`
|
||||
After []string `form:"after" json:"after"`
|
||||
Before []string `form:"before" json:"before"`
|
||||
|
||||
MemoryLimit float64 `form:"memory_limit" json:"memory_limit"`
|
||||
CPUQuota string `form:"cpu_quota" json:"cpu_quota"`
|
||||
|
||||
NoNewPrivileges bool `form:"no_new_privileges" json:"no_new_privileges"`
|
||||
ProtectTmp bool `form:"protect_tmp" json:"protect_tmp"`
|
||||
ProtectHome bool `form:"protect_home" json:"protect_home"`
|
||||
ProtectSystem string `form:"protect_system" json:"protect_system"`
|
||||
ReadWritePaths []string `form:"read_write_paths" json:"read_write_paths"`
|
||||
ReadOnlyPaths []string `form:"read_only_paths" json:"read_only_paths"`
|
||||
}
|
||||
@@ -50,4 +50,13 @@ func init() {
|
||||
)
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20260110-add-project",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&biz.Project{})
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(&biz.Project{})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type Http struct {
|
||||
home *service.HomeService
|
||||
task *service.TaskService
|
||||
website *service.WebsiteService
|
||||
project *service.ProjectService
|
||||
database *service.DatabaseService
|
||||
databaseServer *service.DatabaseServerService
|
||||
databaseUser *service.DatabaseUserService
|
||||
@@ -61,6 +62,7 @@ func NewHttp(
|
||||
home *service.HomeService,
|
||||
task *service.TaskService,
|
||||
website *service.WebsiteService,
|
||||
project *service.ProjectService,
|
||||
database *service.DatabaseService,
|
||||
databaseServer *service.DatabaseServerService,
|
||||
databaseUser *service.DatabaseUserService,
|
||||
@@ -99,6 +101,7 @@ func NewHttp(
|
||||
home: home,
|
||||
task: task,
|
||||
website: website,
|
||||
project: project,
|
||||
database: database,
|
||||
databaseServer: databaseServer,
|
||||
databaseUser: databaseUser,
|
||||
@@ -199,6 +202,14 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/{id}/obtain_cert", route.website.ObtainCert)
|
||||
})
|
||||
|
||||
r.Route("/project", func(r chi.Router) {
|
||||
r.Get("/", route.project.List)
|
||||
r.Post("/", route.project.Create)
|
||||
r.Get("/{id}", route.project.Get)
|
||||
r.Put("/{id}", route.project.Update)
|
||||
r.Delete("/{id}", route.project.Delete)
|
||||
})
|
||||
|
||||
r.Route("/database", func(r chi.Router) {
|
||||
r.Get("/", route.database.List)
|
||||
r.Post("/", route.database.Create)
|
||||
|
||||
101
internal/service/project.go
Normal file
101
internal/service/project.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/libtnb/chix"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type ProjectService struct {
|
||||
projectRepo biz.ProjectRepo
|
||||
}
|
||||
|
||||
func NewProjectService(project biz.ProjectRepo) *ProjectService {
|
||||
return &ProjectService{
|
||||
projectRepo: project,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProjectService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
projects, total, err := s.projectRepo.List(req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": projects,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ProjectService) Get(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
project, err := s.projectRepo.Get(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, project)
|
||||
}
|
||||
|
||||
func (s *ProjectService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ProjectCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
project, err := s.projectRepo.Create(req)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, project)
|
||||
}
|
||||
|
||||
func (s *ProjectService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ProjectUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.projectRepo.Update(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *ProjectService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.projectRepo.Delete(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewHomeService,
|
||||
NewMonitorService,
|
||||
NewProcessService,
|
||||
NewProjectService,
|
||||
NewSafeService,
|
||||
NewSettingService,
|
||||
NewSSHService,
|
||||
|
||||
56
pkg/types/project.go
Normal file
56
pkg/types/project.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package types
|
||||
|
||||
type ProjectType string
|
||||
|
||||
const (
|
||||
ProjectTypeGeneral ProjectType = "general"
|
||||
ProjectTypePHP ProjectType = "php"
|
||||
ProjectTypeJava ProjectType = "java"
|
||||
ProjectTypeGo ProjectType = "go"
|
||||
ProjectTypePython ProjectType = "python"
|
||||
ProjectTypeNodejs ProjectType = "nodejs"
|
||||
)
|
||||
|
||||
type ProjectDetail struct {
|
||||
ID uint `json:"id"` // 项目 ID
|
||||
Name string `json:"name"` // 项目名称
|
||||
Type ProjectType `json:"type"` // 项目类型
|
||||
Description string `json:"description"` // 项目描述
|
||||
RootDir string `json:"root_dir"` // 项目路径
|
||||
WorkingDir string `json:"working_dir"` // 运行目录
|
||||
ExecStartPre string `json:"exec_start_pre"` // 启动前命令
|
||||
ExecStartPost string `json:"exec_start_post"` // 启动后命令
|
||||
ExecStart string `json:"exec_start"` // 启动命令
|
||||
ExecStop string `json:"exec_stop"` // 停止命令
|
||||
ExecReload string `json:"exec_reload"` // 重载命令
|
||||
User string `json:"user"` // 运行用户
|
||||
Restart string `json:"restart"` // 重启策略
|
||||
RestartSec string `json:"restart_sec"` // 重启间隔
|
||||
RestartMax int `json:"restart_max"` // 最大重启次数
|
||||
TimeoutStartSec int `json:"timeout_start_sec"` // 启动超时(秒)
|
||||
TimeoutStopSec int `json:"timeout_stop_sec"` // 停止超时(秒)
|
||||
Environments []KV `json:"environments"` // 环境变量
|
||||
StandardOutput string `json:"standard_output"` // 标准输出 journal/file:/path
|
||||
StandardError string `json:"standard_error"` // 标准错误 journal/file:/path
|
||||
Requires []string `json:"requires"` // 依赖服务(强依赖)
|
||||
Wants []string `json:"wants"` // 依赖服务(弱依赖)
|
||||
After []string `json:"after"` // 启动顺序(在...之后启动)
|
||||
Before []string `json:"before"` // 启动顺序(在...之前启动)
|
||||
|
||||
// 运行状态
|
||||
Status string `json:"status"` // 运行状态
|
||||
PID int `json:"pid"` // 进程ID
|
||||
Memory int64 `json:"memory"` // 内存使用(字节)
|
||||
CPU float64 `json:"cpu"` // CPU使用率
|
||||
Uptime string `json:"uptime"` // 运行时间
|
||||
MemoryLimit float64 `json:"memory_limit"` // 内存限制(字节)
|
||||
CPUQuota float64 `json:"cpu_quota"` // CPU限制(百分比)
|
||||
|
||||
// 安全相关
|
||||
NoNewPrivileges bool `json:"no_new_privileges"` // 无新特权
|
||||
ProtectTmp bool `json:"protect_tmp"` // 保护临时目录
|
||||
ProtectHome bool `json:"protect_home"` // 保护主目录
|
||||
ProtectSystem string `json:"protect_system"` // 保护系统 full/strict
|
||||
ReadWritePaths []string `json:"read_write_paths"` // 读写路径
|
||||
ReadOnlyPaths []string `json:"read_only_paths"` // 只读路径
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const currentTab = ref('general')
|
||||
<n-tab name="general" :tab="$gettext('General')" />
|
||||
<n-tab name="php" :tab="$gettext('PHP')" />
|
||||
<n-tab name="java" :tab="$gettext('Java')" />
|
||||
<n-tab name="go" :tab="$gettext('go')" />
|
||||
<n-tab name="python" :tab="$gettext('Python')" />
|
||||
<n-tab name="nodejs" :tab="$gettext('Node.js')" />
|
||||
</n-tabs>
|
||||
|
||||
Reference in New Issue
Block a user