mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
refactor: 备份重构
This commit is contained in:
@@ -108,7 +108,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到硬盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
|
||||
@@ -129,11 +129,11 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
var free string
|
||||
free, err = shell.Execf("df -k %s | awk '{print $4}' | tail -n 1", app.Root)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取磁盘空间失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取硬盘空间失败")
|
||||
return
|
||||
}
|
||||
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
|
||||
service.Error(w, http.StatusInternalServerError, "磁盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
|
||||
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
12
internal/biz/backup.go
Normal file
12
internal/biz/backup.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package biz
|
||||
|
||||
import "github.com/TheTNB/panel/pkg/types"
|
||||
|
||||
type BackupRepo interface {
|
||||
List(typ string) ([]*types.BackupFile, error)
|
||||
Create(typ, target string, path ...string) error
|
||||
Delete(typ, name string) error
|
||||
CleanExpired(path, prefix string, save int) error
|
||||
CutoffLog(path, target string) error
|
||||
GetPath(typ string) (string, error)
|
||||
}
|
||||
466
internal/data/backup.go
Normal file
466
internal/data/backup.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"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 backupRepo struct {
|
||||
setting biz.SettingRepo
|
||||
website biz.WebsiteRepo
|
||||
}
|
||||
|
||||
func NewBackupRepo() biz.BackupRepo {
|
||||
return &backupRepo{
|
||||
setting: NewSettingRepo(),
|
||||
website: NewWebsiteRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 备份列表
|
||||
func (r *backupRepo) List(typ string) ([]*types.BackupFile, error) {
|
||||
backupPath, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := io.ReadDir(backupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var backupList []*types.BackupFile
|
||||
for _, file := range files {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
backupList = append(backupList, &types.BackupFile{
|
||||
Name: file.Name(),
|
||||
Size: str.FormatBytes(float64(info.Size())),
|
||||
})
|
||||
}
|
||||
|
||||
return backupList, nil
|
||||
}
|
||||
|
||||
// Create 创建备份
|
||||
func (r *backupRepo) Create(typ, target string, path ...string) error {
|
||||
defPath, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(path) > 0 && path[0] != "" {
|
||||
defPath = path[0]
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "website":
|
||||
return r.createWebsite(defPath, target)
|
||||
case "mysql":
|
||||
return r.createMySQL(defPath, target)
|
||||
case "postgres":
|
||||
return r.createPostgres(defPath, target)
|
||||
case "panel":
|
||||
return r.createPanel(defPath)
|
||||
|
||||
}
|
||||
|
||||
return errors.New("未知备份类型")
|
||||
}
|
||||
|
||||
// Delete 删除备份
|
||||
func (r *backupRepo) Delete(typ, name string) error {
|
||||
path, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(path, name)
|
||||
return io.Remove(file)
|
||||
}
|
||||
|
||||
// CutoffLog 切割日志
|
||||
// path 保存目录绝对路径
|
||||
// target 待切割日志文件绝对路径
|
||||
func (r *backupRepo) CutoffLog(path, target string) error {
|
||||
if !io.Exists(target) {
|
||||
return errors.New("日志文件不存在")
|
||||
}
|
||||
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
|
||||
if err := io.Compress([]string{target}, to, io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Remove(target)
|
||||
}
|
||||
|
||||
// CleanExpired 清理过期备份
|
||||
// path 备份目录绝对路径
|
||||
// prefix 目标文件前缀
|
||||
// save 保存份数
|
||||
func (r *backupRepo) CleanExpired(path, prefix string, save int) error {
|
||||
files, err := io.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var filtered []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), ".zip") {
|
||||
info, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, info)
|
||||
}
|
||||
}
|
||||
|
||||
// 排序所有备份文件,从新到旧
|
||||
slices.SortFunc(filtered, func(a, b os.FileInfo) int {
|
||||
if a.ModTime().After(b.ModTime()) {
|
||||
return -1
|
||||
}
|
||||
if a.ModTime().Before(b.ModTime()) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
if len(filtered) <= save {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 切片保留 save 份,删除剩余
|
||||
toDelete := filtered[save:]
|
||||
for _, file := range toDelete {
|
||||
filePath := filepath.Join(path, file.Name())
|
||||
if err = os.Remove(filePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPath 获取备份路径
|
||||
func (r *backupRepo) GetPath(typ string) (string, error) {
|
||||
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
backupPath = filepath.Join(backupPath, typ)
|
||||
if !io.Exists(backupPath) {
|
||||
if err = io.Mkdir(backupPath, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return backupPath, nil
|
||||
}
|
||||
|
||||
// createWebsite 创建网站备份
|
||||
func (r *backupRepo) createWebsite(to string, name string) error {
|
||||
website, err := r.website.GetByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.preCheckPath(to, website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
|
||||
if _, err = shell.Execf(`cd '%s' && zip -r '%s' .`, website.Path, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMySQL 创建 MySQL 备份
|
||||
func (r *backupRepo) createMySQL(to string, name string) error {
|
||||
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := mysql.DatabaseExists(name); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", name)
|
||||
}
|
||||
size, err := mysql.DatabaseSize(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.preCheckDB(to, size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
|
||||
if _, err = shell.Execf(`mysqldump -u root '%s' > '%s'`, name, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Remove(backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createPostgres 创建 PostgreSQL 备份
|
||||
func (r *backupRepo) createPostgres(to string, name string) error {
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := postgres.DatabaseExist(name); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", name)
|
||||
}
|
||||
size, err := postgres.DatabaseSize(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.preCheckDB(to, size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
|
||||
if _, err = shell.Execf(`su - postgres -c "pg_dump '%s'" > '%s'`, name, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Remove(backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createPanel 创建面板备份
|
||||
func (r *backupRepo) createPanel(to string) error {
|
||||
backup := filepath.Join(to, fmt.Sprintf("panel_%s.zip", time.Now().Format("20060102150405")))
|
||||
|
||||
if err := r.preCheckPath(to, filepath.Join(app.Root, "panel")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Compress([]string{
|
||||
filepath.Join(app.Root, "panel"),
|
||||
"/usr/local/sbin/panel-cli",
|
||||
"/usr/local/etc/panel/config.yml",
|
||||
}, backup, io.Zip)
|
||||
}
|
||||
|
||||
// restoreWebsite 恢复网站备份
|
||||
func (r *backupRepo) restoreWebsite(backup, name string) error {
|
||||
if !io.Exists(backup) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
website, err := r.website.GetByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format, err := io.FormatArchiveByPath(backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Remove(website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.UnCompress(backup, website.Path, format); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod(website.Path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chown(website.Path, "www", "www"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreMySQL 恢复 MySQL 备份
|
||||
func (r *backupRepo) restoreMySQL(backup, name string) error {
|
||||
if !io.Exists(backup) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := mysql.DatabaseExists(name); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", name)
|
||||
}
|
||||
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(backup, ".sql") {
|
||||
backup, err = r.autoUnCompressSQL(backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer io.Remove(filepath.Dir(backup))
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`mysql -u root '%s' < '%s'`, name, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Unsetenv("MYSQL_PWD")
|
||||
}
|
||||
|
||||
// restorePostgres 恢复 PostgreSQL 备份
|
||||
func (r *backupRepo) restorePostgres(backup, name string) error {
|
||||
if !io.Exists(backup) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := postgres.DatabaseExist(name); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", name)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(backup, ".sql") {
|
||||
backup, err = r.autoUnCompressSQL(backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer io.Remove(filepath.Dir(backup))
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`su - postgres -c "psql '%s'" < '%s'`, name, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// preCheckPath 预检空间和 inode 是否足够
|
||||
// to 备份保存目录
|
||||
// path 待备份目录
|
||||
func (r *backupRepo) preCheckPath(to, path string) error {
|
||||
size, err := io.SizeX(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := io.CountX(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
}
|
||||
if uint64(files) > usage.InodesFree {
|
||||
return errors.New("备份目录Inode不足")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// preCheckDB 预检空间和 inode 是否足够
|
||||
// to 备份保存目录
|
||||
// size 数据库大小
|
||||
func (r *backupRepo) preCheckDB(to string, size int64) error {
|
||||
usage, err := disk.Usage(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoUnCompressSQL 自动处理压缩文件
|
||||
func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
|
||||
temp, err := io.TempDir(backup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
format, err := io.FormatArchiveByPath(backup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = io.UnCompress(backup, temp, format); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
backup = "" // 置空,防止干扰后续判断
|
||||
if files, err := os.ReadDir(temp); err == nil {
|
||||
if len(files) != 1 {
|
||||
return "", fmt.Errorf("压缩文件中包含的文件数量不为1,实际为%d", len(files))
|
||||
}
|
||||
if strings.HasSuffix(files[0].Name(), ".sql") {
|
||||
backup = filepath.Join(temp, files[0].Name())
|
||||
}
|
||||
}
|
||||
|
||||
if backup == "" {
|
||||
return "", errors.New("无法找到.sql备份文件")
|
||||
}
|
||||
|
||||
return backup, nil
|
||||
}
|
||||
@@ -60,26 +60,26 @@ func (r *cronRepo) Create(req *request.CronCreate) error {
|
||||
|
||||
var script string
|
||||
if req.Type == "backup" {
|
||||
if len(req.BackupPath) == 0 {
|
||||
req.BackupPath, _ = r.settingRepo.Get(biz.SettingKeyBackupPath)
|
||||
if len(req.BackupPath) == 0 {
|
||||
return errors.New("备份路径不能为空")
|
||||
}
|
||||
req.BackupPath = filepath.Join(req.BackupPath, req.BackupType)
|
||||
}
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
if req.BackupType == "website" {
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子面板 - 数据备份脚本
|
||||
# 耗子面板 - 网站备份脚本
|
||||
|
||||
type=%s
|
||||
path=%s
|
||||
name=%s
|
||||
save=%d
|
||||
panel-cli backup website -n %s -p %s
|
||||
panel-cli backup clear -t website -f %s -s %d -p %s
|
||||
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
if req.BackupType == "mysql" || req.BackupType == "postgres" {
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 执行备份
|
||||
panel backup ${type} ${name} ${path} ${save}
|
||||
`, req.BackupType, req.BackupPath, req.Target, req.Save)
|
||||
# 耗子面板 - 数据库备份脚本
|
||||
|
||||
panel-cli backup database -t %s -n %s -p %s
|
||||
panel-cli backup clear -t %s -f %s -s %d -p %s
|
||||
`, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
}
|
||||
if req.Type == "cutoff" {
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
@@ -87,12 +87,10 @@ export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子面板 - 日志切割脚本
|
||||
|
||||
name=%s
|
||||
save=%d
|
||||
|
||||
# 执行切割
|
||||
panel cutoff ${name} ${save}
|
||||
`, req.Target, req.Save)
|
||||
panel-cli cutoff website -n %s -p %s
|
||||
panel-cli cutoff clear -t website -f %s -s %d -p %s
|
||||
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
|
||||
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
|
||||
|
||||
@@ -66,6 +66,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
}
|
||||
|
||||
setting := new(types.WebsiteSetting)
|
||||
setting.ID = website.ID
|
||||
setting.Name = website.Name
|
||||
setting.Path = website.Path
|
||||
setting.SSL = website.SSL
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// PanelTask 面板每日任务
|
||||
type PanelTask struct {
|
||||
appRepo biz.AppRepo
|
||||
appRepo biz.AppRepo
|
||||
backupRepo biz.BackupRepo
|
||||
}
|
||||
|
||||
func NewPanelTask() *PanelTask {
|
||||
return &PanelTask{
|
||||
appRepo: data.NewAppRepo(),
|
||||
appRepo: data.NewAppRepo(),
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,19 +35,20 @@ func (receiver *PanelTask) Run() {
|
||||
}
|
||||
|
||||
// 备份面板
|
||||
if err := io.Compress([]string{"/www/panel"}, filepath.Join(app.Root, "backup", "panel", "panel-"+time.Now().Format(time.DateOnly)+".zip"), io.Zip); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
if err := receiver.backupRepo.Create("panel", ""); err != nil {
|
||||
app.Logger.Error("备份面板失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理 7 天前的备份
|
||||
if _, err := shell.Execf(`find %s -mtime +7 -name "*.zip" -exec rm -rf {} \;`, filepath.Join(app.Root, "backup", "panel")); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
app.Logger.Error("清理面板备份失败", zap.Error(err))
|
||||
// 清理备份
|
||||
path, err := receiver.backupRepo.GetPath("panel")
|
||||
if err == nil {
|
||||
if err = receiver.backupRepo.CleanExpired(path, "panel_", 10); err != nil {
|
||||
app.Logger.Error("清理面板备份失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 更新商店缓存
|
||||
if err := receiver.appRepo.UpdateCache(); err != nil {
|
||||
if err = receiver.appRepo.UpdateCache(); err != nil {
|
||||
app.Logger.Error("更新商店缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
@@ -98,18 +98,62 @@ func Cli() []*cli.Command {
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "创建新站点",
|
||||
Usage: "创建新网站",
|
||||
Action: cliService.WebsiteCreate,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "网站名称",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "domains",
|
||||
Usage: "与网站关联的域名列表",
|
||||
Aliases: []string{"d"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.UintSliceFlag{
|
||||
Name: "ports",
|
||||
Usage: "网站使用的端口列表",
|
||||
Aliases: []string{"p"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "网站托管的路径(不填则默认路径)",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "php",
|
||||
Usage: "网站使用的 PHP 版本(不填不使用)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "移除站点",
|
||||
Usage: "移除网站",
|
||||
Action: cliService.WebsiteRemove,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "网站名称",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: "删除站点(包括站点目录、同名数据库)",
|
||||
Usage: "删除网站(包括网站目录、同名数据库)",
|
||||
Action: cliService.WebsiteDelete,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "网站名称",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "write",
|
||||
@@ -121,22 +165,91 @@ func Cli() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "backup",
|
||||
Usage: "备份数据",
|
||||
Usage: "数据备份",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "website",
|
||||
Usage: "备份网站",
|
||||
Action: cliService.BackupWebsite,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "网站名称",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "database",
|
||||
Usage: "备份数据库",
|
||||
Action: cliService.BackupDatabase,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "数据库类型",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "数据库名称",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "panel",
|
||||
Usage: "备份面板",
|
||||
Action: cliService.BackupPanel,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: "清理备份",
|
||||
Action: cliService.BackupClear,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "备份类型",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "备份文件",
|
||||
Required: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "save",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "保存份数",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "备份目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -148,6 +261,50 @@ func Cli() []*cli.Command {
|
||||
Name: "website",
|
||||
Usage: "网站",
|
||||
Action: cliService.CutoffWebsite,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "网站名称",
|
||||
Required: true,
|
||||
},
|
||||
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: "清理切割的日志",
|
||||
Action: cliService.CutoffClear,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "切割类型",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "切割文件",
|
||||
Required: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "save",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "保存份数",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "切割目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
package service
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
)
|
||||
|
||||
type BackupService struct {
|
||||
backupRepo biz.BackupRepo
|
||||
}
|
||||
|
||||
func NewBackupService() *BackupService {
|
||||
return &BackupService{}
|
||||
return &BackupService{
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BackupService) List(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
@@ -25,33 +27,54 @@ import (
|
||||
)
|
||||
|
||||
type CliService struct {
|
||||
api *api.API
|
||||
app biz.AppRepo
|
||||
user biz.UserRepo
|
||||
setting biz.SettingRepo
|
||||
hash hash.Hasher
|
||||
hr string
|
||||
api *api.API
|
||||
appRepo biz.AppRepo
|
||||
userRepo biz.UserRepo
|
||||
settingRepo biz.SettingRepo
|
||||
backupRepo biz.BackupRepo
|
||||
websiteRepo biz.WebsiteRepo
|
||||
hash hash.Hasher
|
||||
}
|
||||
|
||||
func NewCliService() *CliService {
|
||||
return &CliService{
|
||||
api: api.NewAPI(app.Version),
|
||||
app: data.NewAppRepo(),
|
||||
user: data.NewUserRepo(),
|
||||
setting: data.NewSettingRepo(),
|
||||
hash: hash.NewArgon2id(),
|
||||
hr: `+----------------------------------------------------`,
|
||||
api: api.NewAPI(app.Version),
|
||||
appRepo: data.NewAppRepo(),
|
||||
userRepo: data.NewUserRepo(),
|
||||
settingRepo: data.NewSettingRepo(),
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
websiteRepo: data.NewWebsiteRepo(),
|
||||
hash: hash.NewArgon2id(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CliService) Restart(ctx context.Context, cmd *cli.Command) error {
|
||||
return systemctl.Restart("panel")
|
||||
if err := systemctl.Restart("panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("面板服务已重启")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) Stop(ctx context.Context, cmd *cli.Command) error {
|
||||
return systemctl.Stop("panel")
|
||||
if err := systemctl.Stop("panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("面板服务已停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) Start(ctx context.Context, cmd *cli.Command) error {
|
||||
return systemctl.Start("panel")
|
||||
if err := systemctl.Start("panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("面板服务已启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -66,7 +89,7 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
ver, url, checksum := panel.Version, download.URL, download.Checksum
|
||||
|
||||
return s.setting.UpdatePanel(ver, url, checksum)
|
||||
return s.settingRepo.UpdatePanel(ver, url, checksum)
|
||||
}
|
||||
|
||||
func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -153,6 +176,7 @@ func (s *CliService) UserName(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("用户名修改失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("用户 %s 修改为 %s 成功", oldUsername, newUsername))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -184,6 +208,7 @@ func (s *CliService) UserPassword(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("密码修改失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("用户 %s 密码修改成功", username))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -209,6 +234,7 @@ func (s *CliService) HTTPSOn(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已开启HTTPS")
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -234,6 +260,7 @@ func (s *CliService) HTTPSOff(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已关闭HTTPS")
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -259,6 +286,8 @@ func (s *CliService) EntranceOn(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已开启访问入口")
|
||||
color.Greenln(fmt.Sprintf("访问入口:%s", config.HTTP.Entrance))
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -284,6 +313,7 @@ func (s *CliService) EntranceOff(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已关闭访问入口")
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -314,46 +344,191 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("已修改端口为 %d", port))
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
var ports []uint
|
||||
for _, port := range cmd.IntSlice("ports") {
|
||||
if port < 1 || port > 65535 {
|
||||
return fmt.Errorf("端口范围错误")
|
||||
}
|
||||
ports = append(ports, uint(port))
|
||||
}
|
||||
req := &request.WebsiteCreate{
|
||||
Name: cmd.String("name"),
|
||||
Domains: cmd.StringSlice("domains"),
|
||||
Ports: ports,
|
||||
Path: cmd.String("path"),
|
||||
PHP: int(cmd.Int("php")),
|
||||
DB: false,
|
||||
}
|
||||
|
||||
website, err := s.websiteRepo.Create(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("网站 %s 创建成功", website.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteRemove(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
website, err := s.websiteRepo.GetByName(cmd.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &request.WebsiteDelete{
|
||||
ID: website.ID,
|
||||
}
|
||||
|
||||
if err = s.websiteRepo.Delete(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("网站 %s 移除成功", website.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteDelete(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
website, err := s.websiteRepo.GetByName(cmd.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &request.WebsiteDelete{
|
||||
ID: website.ID,
|
||||
Path: true,
|
||||
DB: true,
|
||||
}
|
||||
|
||||
if err = s.websiteRepo.Delete(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("网站 %s 删除成功", website.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteWrite(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
println("not support")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupWebsite(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-备份类型:网站")
|
||||
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
|
||||
if err := s.backupRepo.Create("website", cmd.String("name"), cmd.String("path")); err != nil {
|
||||
return fmt.Errorf("|-备份失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupDatabase(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-备份类型:数据库")
|
||||
color.Greenln(fmt.Sprintf("|-数据库:%s", cmd.String("type")))
|
||||
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
|
||||
if err := s.backupRepo.Create(cmd.String("type"), cmd.String("name"), cmd.String("path")); err != nil {
|
||||
return fmt.Errorf("|-备份失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupPanel(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-备份类型:面板")
|
||||
if err := s.backupRepo.Create("panel", "", cmd.String("path")); err != nil {
|
||||
return fmt.Errorf("|-备份失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupClear(ctx context.Context, cmd *cli.Command) error {
|
||||
path, err := s.backupRepo.GetPath(cmd.String("type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.String("path") != "" {
|
||||
path = cmd.String("path")
|
||||
}
|
||||
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始清理 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("|-清理类型:%s", cmd.String("type")))
|
||||
color.Greenln(fmt.Sprintf("|-清理目标:%s", cmd.String("file")))
|
||||
color.Greenln(fmt.Sprintf("|-保留份数:%d", cmd.Int("save")))
|
||||
if err = s.backupRepo.CleanExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
|
||||
return fmt.Errorf("|-清理失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 清理成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) CutoffWebsite(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
website, err := s.websiteRepo.GetByName(cmd.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(app.Root, "wwwlogs")
|
||||
if cmd.String("path") != "" {
|
||||
path = cmd.String("path")
|
||||
}
|
||||
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始切割日志 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-切割类型:网站")
|
||||
color.Greenln(fmt.Sprintf("|-切割目标:%s", website.Name))
|
||||
if err = s.backupRepo.CutoffLog(path, filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
|
||||
return err
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 切割成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) CutoffClear(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.String("type") != "website" {
|
||||
return errors.New("当前仅支持网站日志切割")
|
||||
}
|
||||
path := filepath.Join(app.Root, "wwwlogs")
|
||||
if cmd.String("path") != "" {
|
||||
path = cmd.String("path")
|
||||
}
|
||||
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始清理切割日志 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("|-清理类型:%s", cmd.String("type")))
|
||||
color.Greenln(fmt.Sprintf("|-清理目标:%s", cmd.String("file")))
|
||||
color.Greenln(fmt.Sprintf("|-保留份数:%d", cmd.Int("save")))
|
||||
if err := s.backupRepo.CleanExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
|
||||
return err
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 清理成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -364,7 +539,7 @@ func (s *CliService) AppInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("参数不能为空")
|
||||
}
|
||||
|
||||
if err := s.app.Install(channel, slug); err != nil {
|
||||
if err := s.appRepo.Install(channel, slug); err != nil {
|
||||
return fmt.Errorf("应用安装失败:%v", err)
|
||||
}
|
||||
|
||||
@@ -379,7 +554,7 @@ func (s *CliService) AppUnInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("参数不能为空")
|
||||
}
|
||||
|
||||
if err := s.app.UnInstall(slug); err != nil {
|
||||
if err := s.appRepo.UnInstall(slug); err != nil {
|
||||
return fmt.Errorf("应用卸载失败:%v", err)
|
||||
}
|
||||
|
||||
@@ -496,7 +671,14 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("已经初始化过了")
|
||||
}
|
||||
|
||||
settings := []biz.Setting{{Key: biz.SettingKeyName, Value: "耗子面板"}, {Key: biz.SettingKeyMonitor, Value: "1"}, {Key: biz.SettingKeyMonitorDays, Value: "30"}, {Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")}, {Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")}, {Key: biz.SettingKeyVersion, Value: app.Version}}
|
||||
settings := []biz.Setting{
|
||||
{Key: biz.SettingKeyName, Value: "耗子面板"},
|
||||
{Key: biz.SettingKeyMonitor, Value: "1"},
|
||||
{Key: biz.SettingKeyMonitorDays, Value: "30"},
|
||||
{Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")},
|
||||
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")},
|
||||
{Key: biz.SettingKeyVersion, Value: app.Version},
|
||||
}
|
||||
if err := app.Orm.Create(&settings).Error; err != nil {
|
||||
return fmt.Errorf("初始化失败:%v", err)
|
||||
}
|
||||
@@ -534,5 +716,5 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
// 初始化应用中心缓存
|
||||
return s.app.UpdateCache()
|
||||
return s.appRepo.UpdateCache()
|
||||
}
|
||||
|
||||
@@ -72,6 +72,31 @@ func (m *MySQL) DatabaseDrop(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *MySQL) DatabaseExists(name string) (bool, error) {
|
||||
rows, err := m.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var database string
|
||||
if err := rows.Scan(&database); err != nil {
|
||||
continue
|
||||
}
|
||||
if database == name {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *MySQL) DatabaseSize(name string) (int64, error) {
|
||||
var size int64
|
||||
err := m.QueryRow(fmt.Sprintf("SELECT SUM(data_length + index_length) FROM information_schema.tables WHERE table_schema = '%s'", name)).Scan(&size)
|
||||
return size, err
|
||||
}
|
||||
|
||||
func (m *MySQL) UserCreate(user, password string) error {
|
||||
_, err := m.Exec(fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'localhost' IDENTIFIED BY '%s'", user, password))
|
||||
m.flushPrivileges()
|
||||
|
||||
@@ -77,6 +77,23 @@ func (m *Postgres) DatabaseDrop(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Postgres) DatabaseExist(name string) (bool, error) {
|
||||
var count int
|
||||
if err := m.QueryRow("SELECT COUNT(*) FROM pg_database WHERE datname = $1", name).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (m *Postgres) DatabaseSize(name string) (int64, error) {
|
||||
query := fmt.Sprintf("SELECT pg_database_size('%s')", name)
|
||||
var size int64
|
||||
if err := m.QueryRow(query).Scan(&size); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (m *Postgres) UserCreate(user, password string) error {
|
||||
_, err := m.Exec(fmt.Sprintf("CREATE USER %s WITH PASSWORD '%s'", user, password))
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Remove 删除文件/目录
|
||||
@@ -158,3 +160,29 @@ func IsDir(path string) bool {
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// SizeX 获取路径大小(du命令)
|
||||
func SizeX(path string) (int64, error) {
|
||||
out, err := exec.Command("du", "-sb", path).Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
parts := strings.Fields(string(out))
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("无法解析 du 输出")
|
||||
}
|
||||
|
||||
return strconv.ParseInt(parts[0], 10, 64)
|
||||
}
|
||||
|
||||
// CountX 统计目录下文件数
|
||||
func CountX(path string) (int64, error) {
|
||||
out, err := exec.Command("find", path, "-printf", ".").Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := len(string(out))
|
||||
return int64(count), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package types
|
||||
|
||||
// WebsiteSetting 网站设置
|
||||
type WebsiteSetting struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Domains []string `json:"domains"`
|
||||
Ports []uint `json:"ports"`
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"total": "累计上行 { sent } / 累计下行 { received }"
|
||||
},
|
||||
"disk": {
|
||||
"title": "磁盘",
|
||||
"title": "硬盘",
|
||||
"current": "实时读取 { read }/s / 实时写入 { write }/s",
|
||||
"total": "累计读取 { read } / 累计写入 { write }"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ const getRealtime = async () => {
|
||||
netTotalRecv.value = netTotalRecvTemp
|
||||
netCurrentSent.value = (netTotalSent.value - netTotalSentOld) / 3
|
||||
netCurrentRecv.value = (netTotalRecv.value - netTotalRecvOld) / 3
|
||||
// 计算磁盘读写
|
||||
// 计算硬盘读写
|
||||
let diskTotalReadTemp = 0
|
||||
let diskTotalWriteTemp = 0
|
||||
let diskTotalReadOld = diskTotalRead.value
|
||||
|
||||
@@ -10,6 +10,7 @@ const route = useRoute()
|
||||
const { id } = route.params
|
||||
|
||||
const setting = ref<WebsiteSetting>({
|
||||
if: 0,
|
||||
name: '',
|
||||
ports: [],
|
||||
ssl_ports: [],
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Website {
|
||||
}
|
||||
|
||||
export interface WebsiteSetting {
|
||||
id: number
|
||||
name: string
|
||||
ports: number[]
|
||||
ssl_ports: number[]
|
||||
@@ -36,8 +37,3 @@ export interface WebsiteSetting {
|
||||
raw: string
|
||||
log: string
|
||||
}
|
||||
|
||||
export interface Backup {
|
||||
name: string
|
||||
size: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user