mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 提交剩余的插件
This commit is contained in:
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -44,7 +43,7 @@ func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var jails []types.Fail2banJail
|
||||
var jails []Jail
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
@@ -61,7 +60,7 @@ func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
|
||||
jails = append(jails, types.Fail2banJail{
|
||||
jails = append(jails, Jail{
|
||||
Name: jailName,
|
||||
Enabled: jailEnabled,
|
||||
LogPath: jailLogPath[1],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package types
|
||||
package fail2ban
|
||||
|
||||
type Fail2banJail struct {
|
||||
type Jail struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LogPath string `json:"log_path"`
|
||||
@@ -10,6 +10,14 @@ import (
|
||||
_ "github.com/TheTNB/panel/internal/apps/percona"
|
||||
_ "github.com/TheTNB/panel/internal/apps/php"
|
||||
_ "github.com/TheTNB/panel/internal/apps/phpmyadmin"
|
||||
_ "github.com/TheTNB/panel/internal/apps/podman"
|
||||
_ "github.com/TheTNB/panel/internal/apps/postgresql"
|
||||
_ "github.com/TheTNB/panel/internal/apps/pureftpd"
|
||||
_ "github.com/TheTNB/panel/internal/apps/redis"
|
||||
_ "github.com/TheTNB/panel/internal/apps/rsync"
|
||||
_ "github.com/TheTNB/panel/internal/apps/s3fs"
|
||||
_ "github.com/TheTNB/panel/internal/apps/supervisor"
|
||||
_ "github.com/TheTNB/panel/internal/apps/toolbox"
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
)
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ func (s *Service) ExtensionList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ionCube 只支持 PHP 8.3 以下版本
|
||||
if cast.ToUint(s.version) < 83 {
|
||||
extensions = append(extensions, types.PHPExtension{
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
|
||||
@@ -248,7 +248,7 @@ func (s *Service) ExtensionList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
extensionMap := make(map[string]*types.PHPExtension)
|
||||
extensionMap := make(map[string]*Extension)
|
||||
for i := range extensions {
|
||||
extensionMap[extensions[i].Slug] = &extensions[i]
|
||||
}
|
||||
@@ -345,8 +345,8 @@ func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *Service) getExtensions() []types.PHPExtension {
|
||||
extensions := []types.PHPExtension{
|
||||
func (s *Service) getExtensions() []Extension {
|
||||
extensions := []Extension{
|
||||
{
|
||||
Name: "fileinfo",
|
||||
Slug: "fileinfo",
|
||||
@@ -476,7 +476,7 @@ func (s *Service) getExtensions() []types.PHPExtension {
|
||||
|
||||
// ionCube 只支持 PHP 8.3 以下版本
|
||||
if cast.ToUint(s.version) < 83 {
|
||||
extensions = append(extensions, types.PHPExtension{
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
|
||||
@@ -485,7 +485,7 @@ func (s *Service) getExtensions() []types.PHPExtension {
|
||||
}
|
||||
|
||||
raw, _ := shell.Execf("%s/server/php/%d/bin/php -m", panel.Root, s.version)
|
||||
extensionMap := make(map[string]*types.PHPExtension)
|
||||
extensionMap := make(map[string]*Extension)
|
||||
for i := range extensions {
|
||||
extensionMap[extensions[i].Slug] = &extensions[i]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package types
|
||||
package php
|
||||
|
||||
type PHPExtension struct {
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
21
internal/apps/podman/init.go
Normal file
21
internal/apps/podman/init.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package podman
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "podman",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/registryConfig", service.GetRegistryConfig)
|
||||
r.Post("/registryConfig", service.UpdateRegistryConfig)
|
||||
r.Get("/storageConfig", service.GetStorageConfig)
|
||||
r.Post("/storageConfig", service.UpdateStorageConfig)
|
||||
},
|
||||
})
|
||||
}
|
||||
5
internal/apps/podman/request.go
Normal file
5
internal/apps/podman/request.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package podman
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
75
internal/apps/podman/service.go
Normal file
75
internal/apps/podman/service.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package podman
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) GetRegistryConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/containers/registries.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateRegistryConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/containers/registries.conf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("podman"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *Service) GetStorageConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/containers/storage.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStorageConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/containers/storage.conf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("podman"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
24
internal/apps/postgresql/init.go
Normal file
24
internal/apps/postgresql/init.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "postgresql",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/userConfig", service.GetUserConfig)
|
||||
r.Post("/userConfig", service.UpdateUserConfig)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/log", service.Log)
|
||||
r.Post("/clearLog", service.ClearLog)
|
||||
},
|
||||
})
|
||||
}
|
||||
5
internal/apps/postgresql/request.go
Normal file
5
internal/apps/postgresql/request.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package postgresql
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
153
internal/apps/postgresql/service.go
Normal file
153
internal/apps/postgresql/service.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-module/carbon/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/panel"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", panel.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := io.Write(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", panel.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "重载服务失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (s *Service) GetUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", panel.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateUserConfig 保存用户配置
|
||||
func (s *Service) UpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := io.Write(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", panel.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "重载服务失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("postgresql")
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
time, err := shell.Execf(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
|
||||
return
|
||||
}
|
||||
pid, err := shell.Execf(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
|
||||
return
|
||||
}
|
||||
process, err := shell.Execf(`ps aux | grep postgres | grep -v grep | wc -l`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
|
||||
return
|
||||
}
|
||||
connections, err := shell.Execf(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
|
||||
return
|
||||
}
|
||||
storage, err := shell.Execf(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()},
|
||||
{Name: "进程 PID", Value: pid},
|
||||
{Name: "进程数", Value: process},
|
||||
{Name: "总连接数", Value: connections},
|
||||
{Name: "空间占用", Value: storage},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
|
||||
log, err := shell.Execf("tail -n 100 %s/server/postgresql/logs/postgresql-%s.log", panel.Root, carbon.Now().ToDateString())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, log)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *Service) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if out, err := shell.Execf("rm -rf %s/server/postgresql/logs/postgresql-*.log", panel.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
23
internal/apps/pureftpd/init.go
Normal file
23
internal/apps/pureftpd/init.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package pureftpd
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "pureftpd",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/users", service.List)
|
||||
r.Post("/users", service.Create)
|
||||
r.Delete("/users/{name}", service.Delete)
|
||||
r.Post("/users/{name}/password", service.ChangePassword)
|
||||
r.Get("/port", service.GetPort)
|
||||
r.Post("/port", service.UpdatePort)
|
||||
},
|
||||
})
|
||||
}
|
||||
20
internal/apps/pureftpd/request.go
Normal file
20
internal/apps/pureftpd/request.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Username string `form:"username" json:"username"`
|
||||
}
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port"`
|
||||
}
|
||||
173
internal/apps/pureftpd/service.go
Normal file
173
internal/apps/pureftpd/service.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package pureftpd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/panel"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
listRaw, err := shell.Execf("pure-pw list")
|
||||
if err != nil {
|
||||
service.Success(w, chix.M{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []User
|
||||
for _, v := range listArr {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, User{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, users)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Create](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.Path, "/") {
|
||||
req.Path = "/" + req.Path
|
||||
}
|
||||
if !io.Exists(req.Path) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "目录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Chmod(req.Path, 0755); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "修改目录权限失败")
|
||||
return
|
||||
}
|
||||
if err = io.Chown(req.Path, "www", "www"); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "修改目录权限失败")
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf(`yes '%s' | pure-pw useradd '%s' -u www -g www -d '%s'`, req.Password, req.Username, req.Path); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (s *Service) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf("pure-pw userdel '%s' -m", req.Username); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *Service) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ChangePassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`yes '%s' | pure-pw passwd '%s' -m`, req.Password, req.Username); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetPort 获取端口
|
||||
func (s *Service) GetPort(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, panel.Root)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取PureFtpd端口失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, cast.ToInt(port))
|
||||
}
|
||||
|
||||
// UpdatePort 设置端口
|
||||
func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdatePort](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%d/g" %s/server/pure-ftpd/etc/pure-ftpd.conf`, req.Port, panel.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Port: req.Port,
|
||||
Protocol: "tcp",
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("pure-ftpd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package types
|
||||
package pureftpd
|
||||
|
||||
type PureFtpdUser struct {
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
20
internal/apps/redis/init.go
Normal file
20
internal/apps/redis/init.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "redis",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
},
|
||||
})
|
||||
}
|
||||
5
internal/apps/redis/request.go
Normal file
5
internal/apps/redis/request.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package redis
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
96
internal/apps/redis/service.go
Normal file
96
internal/apps/redis/service.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/TheTNB/panel/internal/panel"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("redis")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 Redis 状态失败")
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("redis-cli info")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 Redis 负载失败")
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: "TCP 端口", Value: dataRaw["tcp_port"]},
|
||||
{Name: "已运行天数", Value: dataRaw["uptime_in_days"]},
|
||||
{Name: "连接的客户端数", Value: dataRaw["connected_clients"]},
|
||||
{Name: "已分配的内存总量", Value: dataRaw["used_memory_human"]},
|
||||
{Name: "占用内存总量", Value: dataRaw["used_memory_rss_human"]},
|
||||
{Name: "占用内存峰值", Value: dataRaw["used_memory_peak_human"]},
|
||||
{Name: "内存碎片比率", Value: dataRaw["mem_fragmentation_ratio"]},
|
||||
{Name: "运行以来连接过的客户端的总数", Value: dataRaw["total_connections_received"]},
|
||||
{Name: "运行以来执行过的命令的总数", Value: dataRaw["total_commands_processed"]},
|
||||
{Name: "每秒执行的命令数", Value: dataRaw["instantaneous_ops_per_sec"]},
|
||||
{Name: "查找数据库键成功次数", Value: dataRaw["keyspace_hits"]},
|
||||
{Name: "查找数据库键失败次数", Value: dataRaw["keyspace_misses"]},
|
||||
{Name: "最近一次 fork() 操作耗费的毫秒数", Value: dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", panel.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/redis/redis.conf", panel.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("redis"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
23
internal/apps/rsync/init.go
Normal file
23
internal/apps/rsync/init.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "rsync",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/modules", service.List)
|
||||
r.Post("/modules", service.Create)
|
||||
r.Post("/modules/{name}", service.Update)
|
||||
r.Delete("/modules/{name}", service.Delete)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
},
|
||||
})
|
||||
}
|
||||
27
internal/apps/rsync/request.go
Normal file
27
internal/apps/rsync/request.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package rsync
|
||||
|
||||
type Create struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
319
internal/apps/rsync/service.go
Normal file
319
internal/apps/rsync/service.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 列出模块
|
||||
// @Description 列出所有 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules [get]
|
||||
func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var modules []Module
|
||||
lines := strings.Split(config, "\n")
|
||||
var currentModule *Module
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
moduleName := line[1 : len(line)-1]
|
||||
currentModule = &Module{
|
||||
Name: moduleName,
|
||||
}
|
||||
} else if currentModule != nil {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "path":
|
||||
currentModule.Path = value
|
||||
case "comment":
|
||||
currentModule.Comment = value
|
||||
case "read only":
|
||||
currentModule.ReadOnly = value == "yes" || value == "true"
|
||||
case "auth users":
|
||||
currentModule.AuthUser = value
|
||||
currentModule.Secret, err = shell.Execf("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败")
|
||||
return
|
||||
}
|
||||
case "hosts allow":
|
||||
currentModule.HostsAllow = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, modules)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Create
|
||||
//
|
||||
// @Summary 添加模块
|
||||
// @Description 添加 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Create true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules [post]
|
||||
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Create](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.Contains(config, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "模块 "+req.Name+" 已存在")
|
||||
return
|
||||
}
|
||||
|
||||
conf := `# ` + req.Name + `-START
|
||||
[` + req.Name + `]
|
||||
path = ` + req.Path + `
|
||||
comment = ` + req.Comment + `
|
||||
read only = no
|
||||
auth users = ` + req.AuthUser + `
|
||||
hosts allow = ` + req.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END
|
||||
`
|
||||
|
||||
if err = io.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("echo '" + req.AuthUser + ":" + req.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Delete
|
||||
//
|
||||
// @Summary 删除模块
|
||||
// @Description 删除 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param name path string true "模块名称"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules/{name} [delete]
|
||||
func (s *Service) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !strings.Contains(config, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "模块 "+req.Name+" 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
config = strings.Replace(config, "\n# "+req.Name+"-START"+module+"# "+req.Name+"-END", "", -1)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Update
|
||||
//
|
||||
// @Summary 更新模块
|
||||
// @Description 更新 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param name path string true "模块名称"
|
||||
// @Param data body requests.Update true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules/{name} [post]
|
||||
func (s *Service) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Update](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !strings.Contains(config, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "模块 "+req.Name+" 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
newConf := `# ` + req.Name + `-START
|
||||
[` + req.Name + `]
|
||||
path = ` + req.Path + `
|
||||
comment = ` + req.Comment + `
|
||||
read only = no
|
||||
auth users = ` + req.AuthUser + `
|
||||
hosts allow = ` + req.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END`
|
||||
|
||||
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
config = strings.Replace(config, "# "+req.Name+"-START"+module+"# "+req.Name+"-END", newConf, -1)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("echo '" + req.AuthUser + ":" + req.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Description 获取 Rsync 配置
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/config [get]
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig
|
||||
//
|
||||
// @Summary 更新配置
|
||||
// @Description 更新 Rsync 配置
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/config [post]
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package types
|
||||
package rsync
|
||||
|
||||
type RsyncModule struct {
|
||||
type Module struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Comment string `json:"comment"`
|
||||
20
internal/apps/s3fs/init.go
Normal file
20
internal/apps/s3fs/init.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package s3fs
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "s3fs",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/mounts", service.List)
|
||||
r.Post("/mounts", service.Create)
|
||||
r.Delete("/mounts", service.Delete)
|
||||
},
|
||||
})
|
||||
}
|
||||
13
internal/apps/s3fs/request.go
Normal file
13
internal/apps/s3fs/request.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package s3fs
|
||||
|
||||
type Create struct {
|
||||
Ak string `form:"ak" json:"ak"`
|
||||
Sk string `form:"sk" json:"sk"`
|
||||
Bucket string `form:"bucket" json:"bucket"`
|
||||
URL string `form:"url" json:"url"`
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
ID int64 `form:"id" json:"id"`
|
||||
}
|
||||
206
internal/apps/s3fs/service.go
Normal file
206
internal/apps/s3fs/service.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package s3fs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{
|
||||
settingRepo: data.NewSettingRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 S3fs 挂载
|
||||
func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
var s3fsList []Mount
|
||||
list, err := s.settingRepo.Get("s3fs", "[]")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.Unmarshal([]byte(list), &s3fsList); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, s3fsList)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Create 添加 S3fs 挂载
|
||||
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Create](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的
|
||||
if strings.Contains(req.URL, req.Bucket) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查挂载目录是否存在且为空
|
||||
if !io.Exists(req.Path) {
|
||||
if err = io.Mkdir(req.Path, 0755); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "挂载目录创建失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
if !io.Empty(req.Path) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "挂载目录必须为空")
|
||||
return
|
||||
}
|
||||
|
||||
var s3fsList []Mount
|
||||
list, err := s.settingRepo.Get("s3fs", "[]")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal([]byte(list), &s3fsList); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range s3fsList {
|
||||
if s.Path == req.Path {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "路径已存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
id := carbon.Now().TimestampMilli()
|
||||
password := req.Ak + ":" + req.Sk
|
||||
if err = io.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
out, err := shell.Execf(`echo 's3fs#%s %s fuse _netdev,allow_other,nonempty,url=%s,passwd_file=/etc/passwd-s3fs-%s 0 0' >> /etc/fstab`, req.Bucket, req.Path, req.URL, cast.ToString(id))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
|
||||
service.Error(w, http.StatusInternalServerError, "/etc/fstab 有误: "+mountCheck)
|
||||
return
|
||||
}
|
||||
if _, err := shell.Execf("df -h | grep " + req.Path + " 2>&1"); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
|
||||
service.Error(w, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
|
||||
return
|
||||
}
|
||||
|
||||
s3fsList = append(s3fsList, Mount{
|
||||
ID: id,
|
||||
Path: req.Path,
|
||||
Bucket: req.Bucket,
|
||||
Url: req.URL,
|
||||
})
|
||||
encoded, err := json.Marshal(s3fsList)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.settingRepo.Set("s3fs", string(encoded)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Delete 删除 S3fs 挂载
|
||||
func (s *Service) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var s3fsList []Mount
|
||||
list, err := s.settingRepo.Get("s3fs", "[]")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal([]byte(list), &s3fsList); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
var mount Mount
|
||||
for _, item := range s3fsList {
|
||||
if item.ID == req.ID {
|
||||
mount = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "挂载不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf(`umount '` + mount.Path + `' 2>&1`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "/etc/fstab 有误: "+mountCheck)
|
||||
return
|
||||
}
|
||||
if err = io.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var newS3fsList []Mount
|
||||
for _, item := range s3fsList {
|
||||
if item.ID != mount.ID {
|
||||
newS3fsList = append(newS3fsList, item)
|
||||
}
|
||||
}
|
||||
encoded, err := json.Marshal(newS3fsList)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
if err = s.settingRepo.Set("s3fs", string(encoded)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package types
|
||||
package s3fs
|
||||
|
||||
type S3fsMount struct {
|
||||
type Mount struct {
|
||||
ID int64 `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Bucket string `json:"bucket"`
|
||||
32
internal/apps/supervisor/init.go
Normal file
32
internal/apps/supervisor/init.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "supervisor",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/service", service.Service)
|
||||
r.Get("/log", service.Log)
|
||||
r.Post("/clearLog", service.ClearLog)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/processes", service.Processes)
|
||||
r.Post("/processes/{name}/start", service.StartProcess)
|
||||
r.Post("/processes/{name}/stop", service.StopProcess)
|
||||
r.Post("/processes/{name}/restart", service.RestartProcess)
|
||||
r.Get("/processes/{name}/log", service.ProcessLog)
|
||||
r.Post("/processes/{name}/clearLog", service.ClearProcessLog)
|
||||
r.Get("/processes/{name}", service.ProcessConfig)
|
||||
r.Post("/processes/{name}", service.UpdateProcessConfig)
|
||||
r.Delete("/processes/{name}", service.DeleteProcess)
|
||||
r.Post("/processes", service.CreateProcess)
|
||||
},
|
||||
})
|
||||
}
|
||||
22
internal/apps/supervisor/request.go
Normal file
22
internal/apps/supervisor/request.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package supervisor
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
|
||||
type UpdateProcessConfig struct {
|
||||
Process string `form:"config" json:"process"`
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
|
||||
type ProcessName struct {
|
||||
Process string `form:"config" json:"process"`
|
||||
}
|
||||
|
||||
type CreateProcess struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
User string `form:"user" json:"user"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Command string `form:"command" json:"command"`
|
||||
Num int `form:"num" json:"num"`
|
||||
}
|
||||
379
internal/apps/supervisor/service.go
Normal file
379
internal/apps/supervisor/service.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
var name string
|
||||
if os.IsRHEL() {
|
||||
name = "supervisord"
|
||||
} else {
|
||||
name = "supervisor"
|
||||
}
|
||||
|
||||
return &Service{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// Service 获取服务名称
|
||||
func (s *Service) Service(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.name)
|
||||
}
|
||||
|
||||
// Log 日志
|
||||
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
|
||||
log, err := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, log)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *Service) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if out, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var config string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/supervisord.conf`, req.Config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart(s.name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", s.name))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
func (s *Service) Processes(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
var processes []Process
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p Process
|
||||
p.Name = line
|
||||
if status, err := shell.Execf(`supervisorctl status '%s' | awk '{print $2}'`, line); err == nil {
|
||||
p.Status = status
|
||||
}
|
||||
if p.Status == "RUNNING" {
|
||||
pid, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $4}'`, line)
|
||||
p.Pid = strings.ReplaceAll(pid, ",", "")
|
||||
uptime, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $6}'`, line)
|
||||
p.Uptime = uptime
|
||||
} else {
|
||||
p.Pid = "-"
|
||||
p.Uptime = "-"
|
||||
}
|
||||
processes = append(processes, p)
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, processes)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// StartProcess 启动进程
|
||||
func (s *Service) StartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl start %s`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StopProcess 停止进程
|
||||
func (s *Service) StopProcess(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop %s`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// RestartProcess 重启进程
|
||||
func (s *Service) RestartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl restart %s`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ProcessLog 进程日志
|
||||
func (s *Service) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, fmt.Sprintf("无法从进程 %s 的配置文件中获取日志路径", req.Process))
|
||||
return
|
||||
}
|
||||
|
||||
log, err := shell.Execf(`tail -n 200 '%s'`, logPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, log)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, log)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (s *Service) ClearProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, fmt.Sprintf("无法从进程 %s 的配置文件中获取日志路径", req.Process))
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`echo "" > '%s'`, logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (s *Service) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var config string
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.d/` + req.Process + `.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + req.Process + `.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateProcessConfig 保存进程配置
|
||||
func (s *Service) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateProcessConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s'`, req.Process)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// CreateProcess 添加进程
|
||||
func (s *Service) CreateProcess(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[CreateProcess](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config := `[program:` + req.Name + `]
|
||||
command=` + req.Command + `
|
||||
process_name=%(program_name)s
|
||||
directory=` + req.Path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=` + req.User + `
|
||||
numprocs=` + cast.ToString(req.Num) + `
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/` + req.Name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Name+`.conf`, config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Name+`.conf`, config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s'`, req.Name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DeleteProcess 删除进程
|
||||
func (s *Service) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, out)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
if err := io.Remove(`/etc/supervisord.d/` + req.Process + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
if err := io.Remove(`/etc/supervisor/conf.d/` + req.Process + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, fmt.Sprintf("无法从进程 %s 的配置文件中获取日志路径", req.Process))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
8
internal/apps/supervisor/types.go
Normal file
8
internal/apps/supervisor/types.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package supervisor
|
||||
|
||||
type Process struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Pid string `json:"pid"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
26
internal/apps/toolbox/init.go
Normal file
26
internal/apps/toolbox/init.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "toolbox",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/dns", service.GetDNS)
|
||||
r.Post("/dns", service.UpdateDNS)
|
||||
r.Get("/swap", service.GetSWAP)
|
||||
r.Post("/swap", service.UpdateSWAP)
|
||||
r.Get("/timezone", service.GetTimezone)
|
||||
r.Post("/timezone", service.UpdateTimezone)
|
||||
r.Get("/hosts", service.GetHosts)
|
||||
r.Post("/hosts", service.UpdateHosts)
|
||||
r.Post("/rootPassword", service.UpdateRootPassword)
|
||||
},
|
||||
})
|
||||
}
|
||||
22
internal/apps/toolbox/request.go
Normal file
22
internal/apps/toolbox/request.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package toolbox
|
||||
|
||||
type DNS struct {
|
||||
DNS1 string `form:"dns1" json:"dns1"`
|
||||
DNS2 string `form:"dns2" json:"dns2"`
|
||||
}
|
||||
|
||||
type SWAP struct {
|
||||
Size int64 `form:"size" json:"size"`
|
||||
}
|
||||
|
||||
type Timezone struct {
|
||||
Timezone string `form:"timezone" json:"timezone"`
|
||||
}
|
||||
|
||||
type Hosts struct {
|
||||
Hosts string `form:"hosts" json:"hosts"`
|
||||
}
|
||||
|
||||
type Password struct {
|
||||
Password string `form:"password" json:"password"`
|
||||
}
|
||||
275
internal/apps/toolbox/service.go
Normal file
275
internal/apps/toolbox/service.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/panel"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// GetDNS 获取 DNS 信息
|
||||
func (s *Service) GetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1)
|
||||
if len(match) == 0 {
|
||||
service.Error(w, http.StatusInternalServerError, "找不到 DNS 信息")
|
||||
return
|
||||
}
|
||||
|
||||
var dns []string
|
||||
for _, m := range match {
|
||||
dns = append(dns, m[1])
|
||||
}
|
||||
|
||||
service.Success(w, dns)
|
||||
}
|
||||
|
||||
// UpdateDNS 设置 DNS 信息
|
||||
func (s *Service) UpdateDNS(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[DNS](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.DNS1) == 0 || len(req.DNS2) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "DNS 信息不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var dns string
|
||||
dns += "nameserver " + req.DNS1 + "\n"
|
||||
dns += "nameserver " + req.DNS2 + "\n"
|
||||
|
||||
if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入 DNS 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetSWAP 获取 SWAP 信息
|
||||
func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
var total, used, free string
|
||||
var size int64
|
||||
if io.Exists(filepath.Join(panel.Root, "swap")) {
|
||||
file, err := io.FileInfo(filepath.Join(panel.Root, "swap"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
size = file.Size() / 1024 / 1024
|
||||
total = str.FormatBytes(float64(file.Size()))
|
||||
} else {
|
||||
size = 0
|
||||
total = "0.00 B"
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("free | grep Swap")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) > 0 {
|
||||
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"size": size,
|
||||
"used": used,
|
||||
"free": free,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSWAP 设置 SWAP 信息
|
||||
func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[SWAP](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(filepath.Join(panel.Root, "swap")) {
|
||||
if out, err := shell.Execf("swapoff '%s'", filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("rm -f '%s'", filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf(`sed -i '%s/d' /etc/fstab`, filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Size > 1 {
|
||||
free, err := shell.Execf("df -k %s | awk '{print $4}' | tail -n 1", panel.Root)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取磁盘空间失败")
|
||||
return
|
||||
}
|
||||
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+str.FormatBytes(cast.ToFloat64(free)))
|
||||
return
|
||||
}
|
||||
|
||||
btrfsCheck, _ := shell.Execf("df -T %s | awk '{print $2}' | tail -n 1", panel.Root)
|
||||
if strings.Contains(btrfsCheck, "btrfs") {
|
||||
if out, err := shell.Execf("btrfs filesystem mkswapfile --size %dM --uuid clear %s", req.Size, filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("dd if=/dev/zero of=%s bs=1M count=%d", filepath.Join(panel.Root, "swap"), req.Size); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("mkswap -f '%s'", filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
if err := io.Chmod(filepath.Join(panel.Root, "swap"), 0600); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "设置 SWAP 权限失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
if out, err := shell.Execf("swapon '%s'", filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
if out, err := shell.Execf("echo '%s swap swap defaults 0 0' >> /etc/fstab", filepath.Join(panel.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetTimezone 获取时区
|
||||
func (s *Service) GetTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := shell.Execf("timedatectl | grep zone")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取时区信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "找不到时区信息")
|
||||
return
|
||||
}
|
||||
|
||||
zonesRaw, err := shell.Execf("timedatectl list-timezones")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取时区列表失败")
|
||||
return
|
||||
}
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
|
||||
var zonesList []types.LV
|
||||
for _, z := range zones {
|
||||
zonesList = append(zonesList, types.LV{
|
||||
Label: z,
|
||||
Value: z,
|
||||
})
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"timezone": match[1],
|
||||
"timezones": zonesList,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateTimezone 设置时区
|
||||
func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Timezone](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf("timedatectl set-timezone '%s'", req.Timezone); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
|
||||
hosts, err := io.Read("/etc/hosts")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, hosts)
|
||||
}
|
||||
|
||||
// UpdateHosts 设置 hosts 信息
|
||||
func (s *Service) UpdateHosts(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Hosts](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/hosts", req.Hosts, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "写入 hosts 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateRootPassword 设置 root 密码
|
||||
func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Password](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
return
|
||||
}
|
||||
|
||||
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if out, err := shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, out)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
6
internal/apps/toolbox/types.go
Normal file
6
internal/apps/toolbox/types.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package toolbox
|
||||
|
||||
type zone struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
var plugins sync.Map
|
||||
|
||||
func Register(plugin *types.App) {
|
||||
if _, ok := plugins.Load(plugin.Slug); ok {
|
||||
panic(fmt.Sprintf("plugin %s already exists", plugin.Slug))
|
||||
}
|
||||
plugins.Store(plugin.Slug, plugin)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user