mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 05:31:44 +08:00
549 lines
13 KiB
Go
549 lines
13 KiB
Go
package data
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"slices"
|
||
"strings"
|
||
"time"
|
||
|
||
"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 biz.BackupType) ([]*types.BackupFile, error) {
|
||
path, err := r.GetPath(typ)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
files, err := io.ReadDir(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
list := make([]*types.BackupFile, 0)
|
||
for _, file := range files {
|
||
info, err := file.Info()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
list = append(list, &types.BackupFile{
|
||
Name: file.Name(),
|
||
Path: filepath.Join(path, file.Name()),
|
||
Size: str.FormatBytes(float64(info.Size())),
|
||
Time: info.ModTime(),
|
||
})
|
||
}
|
||
|
||
return list, nil
|
||
}
|
||
|
||
// Create 创建备份
|
||
// typ 备份类型
|
||
// target 目标名称
|
||
// path 可选备份保存路径
|
||
func (r *backupRepo) Create(typ biz.BackupType, 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 biz.BackupTypeWebsite:
|
||
return r.createWebsite(defPath, target)
|
||
case biz.BackupTypeMySQL:
|
||
return r.createMySQL(defPath, target)
|
||
case biz.BackupTypePostgres:
|
||
return r.createPostgres(defPath, target)
|
||
case biz.BackupTypePanel:
|
||
return r.createPanel(defPath)
|
||
|
||
}
|
||
|
||
return errors.New("未知备份类型")
|
||
}
|
||
|
||
// Delete 删除备份
|
||
func (r *backupRepo) Delete(typ biz.BackupType, name string) error {
|
||
path, err := r.GetPath(typ)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
file := filepath.Join(path, name)
|
||
return io.Remove(file)
|
||
}
|
||
|
||
// Restore 恢复备份
|
||
// typ 备份类型
|
||
// backup 备份压缩包,可以是绝对路径或者相对路径
|
||
// target 目标名称
|
||
func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
|
||
if !io.Exists(backup) {
|
||
path, err := r.GetPath(typ)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
backup = filepath.Join(path, backup)
|
||
}
|
||
|
||
switch typ {
|
||
case biz.BackupTypeWebsite:
|
||
return r.restoreWebsite(backup, target)
|
||
case biz.BackupTypeMySQL:
|
||
return r.restoreMySQL(backup, target)
|
||
case biz.BackupTypePostgres:
|
||
return r.restorePostgres(backup, target)
|
||
}
|
||
|
||
return errors.New("未知备份类型")
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// ClearExpired 清理过期备份
|
||
// path 备份目录绝对路径
|
||
// prefix 目标文件前缀
|
||
// save 保存份数
|
||
func (r *backupRepo) ClearExpired(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 app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-清理过期文件:%s", filePath))
|
||
}
|
||
if err = os.Remove(filePath); err != nil {
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-清理失败:%v", err))
|
||
} else {
|
||
return fmt.Errorf("清理失败:%v", err)
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetPath 获取备份路径
|
||
func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
|
||
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if !slices.Contains([]biz.BackupType{biz.BackupTypePath, biz.BackupTypeWebsite, biz.BackupTypeMySQL, biz.BackupTypePostgres, biz.BackupTypeRedis, biz.BackupTypePanel}, typ) {
|
||
return "", errors.New("未知备份类型")
|
||
}
|
||
|
||
backupPath = filepath.Join(backupPath, string(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
|
||
}
|
||
|
||
var paths []string
|
||
dirs, err := io.ReadDir(website.Path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, item := range dirs {
|
||
paths = append(paths, filepath.Join(website.Path, item.Name()))
|
||
}
|
||
|
||
start := time.Now()
|
||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
|
||
if err = io.Compress(paths, backup, io.Zip); err != nil {
|
||
return err
|
||
}
|
||
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||
fmt.Println(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
|
||
}
|
||
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", "unix")
|
||
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
|
||
}
|
||
start := time.Now()
|
||
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
|
||
}
|
||
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||
fmt.Println(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
|
||
}
|
||
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
|
||
}
|
||
|
||
start := time.Now()
|
||
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
|
||
}
|
||
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||
fmt.Println(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
|
||
}
|
||
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
|
||
}
|
||
|
||
start := time.Now()
|
||
if err := io.Compress([]string{
|
||
filepath.Join(app.Root, "panel"),
|
||
"/usr/local/sbin/panel-cli",
|
||
"/usr/local/etc/panel/config.yml",
|
||
}, backup, io.Zip); err != nil {
|
||
return err
|
||
}
|
||
if err := io.Chmod(backup, 0600); err != nil {
|
||
return err
|
||
}
|
||
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||
fmt.Println(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// restoreWebsite 恢复网站备份
|
||
func (r *backupRepo) restoreWebsite(backup, target string) error {
|
||
if !io.Exists(backup) {
|
||
return errors.New("备份文件不存在")
|
||
}
|
||
|
||
website, err := r.website.GetByName(target)
|
||
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, target 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", "unix")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if exist, _ := mysql.DatabaseExists(target); !exist {
|
||
return fmt.Errorf("数据库不存在:%s", target)
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
if _, err = shell.Execf(`mysql -u root '%s' < '%s'`, target, backup); err != nil {
|
||
return err
|
||
}
|
||
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
|
||
return err
|
||
}
|
||
|
||
_ = io.Remove(filepath.Dir(backup))
|
||
return nil
|
||
}
|
||
|
||
// restorePostgres 恢复 PostgreSQL 备份
|
||
func (r *backupRepo) restorePostgres(backup, target 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(target); !exist {
|
||
return fmt.Errorf("数据库不存在:%s", target)
|
||
}
|
||
|
||
if !strings.HasSuffix(backup, ".sql") {
|
||
backup, err = r.autoUnCompressSQL(backup)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if _, err = shell.Execf(`su - postgres -c "psql '%s'" < '%s'`, target, backup); err != nil {
|
||
return err
|
||
}
|
||
|
||
_ = io.Remove(filepath.Dir(backup))
|
||
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
|
||
}
|
||
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||
fmt.Println(fmt.Sprintf("|-目标文件数:%d", files))
|
||
fmt.Println(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||
fmt.Println(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
|
||
}
|
||
|
||
if app.IsCli {
|
||
fmt.Println(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||
fmt.Println(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||
fmt.Println(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
|
||
}
|