2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00
Files
panel/internal/data/backup.go
2025-09-18 23:24:03 +08:00

835 lines
23 KiB
Go

package data
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/leonelquinteros/gotext"
"github.com/shirou/gopsutil/disk"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/app"
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/pkg/db"
"github.com/acepanel/panel/pkg/io"
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/tools"
"github.com/acepanel/panel/pkg/types"
)
type backupRepo struct {
t *gotext.Locale
db *gorm.DB
setting biz.SettingRepo
website biz.WebsiteRepo
}
func NewBackupRepo(t *gotext.Locale, db *gorm.DB, setting biz.SettingRepo, website biz.WebsiteRepo) biz.BackupRepo {
return &backupRepo{
t: t,
db: db,
setting: setting,
website: website,
}
}
// List 备份列表
func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
path, err := r.GetPath(typ)
if err != nil {
return nil, err
}
files, err := os.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: tools.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(r.t.Get("unknown backup type"))
}
// 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(r.t.Get("unknown backup type"))
}
// CutoffLog 切割日志
// path 保存目录绝对路径
// target 待切割日志文件绝对路径
func (r *backupRepo) CutoffLog(path, target string) error {
if !io.Exists(target) {
return errors.New(r.t.Get("log file %s not exists", target))
}
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
if err := io.Compress(filepath.Dir(target), []string{filepath.Base(target)}, to); err != nil {
return err
}
// 原文件不能直接删除,直接删的话仍会占用空间直到重启相关的应用
if _, err := shell.Execf("cat /dev/null > '%s'", target); err != nil {
return err
}
return nil
}
// ClearExpired 清理过期备份
// path 备份目录绝对路径
// prefix 目标文件前缀
// save 保存份数
func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
files, err := os.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(r.t.Get("|-Cleaning expired file: %s", filePath))
}
if err = os.Remove(filePath); err != nil {
return errors.New(r.t.Get("Cleanup failed: %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(r.t.Get("unknown backup type"))
}
backupPath = filepath.Join(backupPath, string(typ))
if !io.Exists(backupPath) {
if err = os.MkdirAll(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
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
if err = io.Compress(website.Path, nil, backup); err != nil {
return err
}
if app.IsCli {
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %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
}
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if exist, _ := mysql.DatabaseExists(name); !exist {
return errors.New(r.t.Get("database does not exist: %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(filepath.Dir(backup), []string{filepath.Base(backup)}, backup+".zip"); err != nil {
return err
}
if err = io.Remove(backup); err != nil {
return err
}
if app.IsCli {
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %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", 5432)
if err != nil {
return err
}
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if exist, _ := postgres.DatabaseExist(name); !exist {
return errors.New(r.t.Get("database does not exist: %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(filepath.Dir(backup), []string{filepath.Base(backup)}, backup+".zip"); err != nil {
return err
}
if err = io.Remove(backup); err != nil {
return err
}
if app.IsCli {
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %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()
temp, err := os.MkdirTemp("", "panel-backup")
if err != nil {
return err
}
if err = io.Cp(filepath.Join(app.Root, "panel"), temp); err != nil {
return err
}
if err = io.Cp("/usr/local/sbin/panel-cli", temp); err != nil {
return err
}
if err = io.Cp("/usr/local/etc/panel/config.yml", temp); err != nil {
return err
}
_ = io.Chmod(temp, 0600)
if err = io.Compress(temp, nil, backup); err != nil {
return err
}
if err = io.Chmod(backup, 0600); err != nil {
return err
}
if app.IsCli {
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %s", filepath.Base(backup)))
}
return io.Remove(temp)
}
// restoreWebsite 恢复网站备份
func (r *backupRepo) restoreWebsite(backup, target string) error {
if !io.Exists(backup) {
return errors.New(r.t.Get("backup file %s not exists", backup))
}
website, err := r.website.GetByName(target)
if err != nil {
return err
}
if err = io.Remove(website.Path); err != nil {
return err
}
if err = io.UnCompress(backup, website.Path); 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(r.t.Get("backup file %s not exists", backup))
}
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
}
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if exist, _ := mysql.DatabaseExists(target); !exist {
return errors.New(r.t.Get("database does not exist: %s", target))
}
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
return err
}
clean := false
if !strings.HasSuffix(backup, ".sql") {
backup, err = r.autoUnCompressSQL(backup)
if err != nil {
return err
}
clean = true
}
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
}
if clean {
_ = io.Remove(filepath.Dir(backup))
}
return nil
}
// restorePostgres 恢复 PostgreSQL 备份
func (r *backupRepo) restorePostgres(backup, target string) error {
if !io.Exists(backup) {
return errors.New(r.t.Get("backup file %s not exists", backup))
}
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432)
if err != nil {
return err
}
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if exist, _ := postgres.DatabaseExist(target); !exist {
return errors.New(r.t.Get("database does not exist: %s", target))
}
clean := false
if !strings.HasSuffix(backup, ".sql") {
backup, err = r.autoUnCompressSQL(backup)
if err != nil {
return err
}
clean = true
}
if _, err = shell.Execf(`su - postgres -c "psql '%s'" < '%s'`, target, backup); err != nil {
return err
}
if clean {
_ = 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(r.t.Get("|-Target size: %s", tools.FormatBytes(float64(size))))
fmt.Println(r.t.Get("|-Target file count: %d", files))
fmt.Println(r.t.Get("|-Backup directory available space: %s", tools.FormatBytes(float64(usage.Free))))
fmt.Println(r.t.Get("|-Backup directory available Inode: %d", usage.InodesFree))
}
if uint64(size) > usage.Free {
return errors.New(r.t.Get("Insufficient backup directory space"))
}
// 对于 fuse 等文件系统,可能没有 inode 的概念
/*if uint64(files) > usage.InodesFree {
return errors.New(r.t.Get("Insufficient backup directory 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(r.t.Get("|-Target size: %s", tools.FormatBytes(float64(size))))
fmt.Println(r.t.Get("|-Backup directory available space: %s", tools.FormatBytes(float64(usage.Free))))
fmt.Println(r.t.Get("|-Backup directory available Inode: %d", usage.InodesFree))
}
if uint64(size) > usage.Free {
return errors.New(r.t.Get("Insufficient backup directory space"))
}
return nil
}
// autoUnCompressSQL 自动处理压缩文件
func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
temp, err := os.MkdirTemp("", "sql-uncompress")
if err != nil {
return "", err
}
if err = io.UnCompress(backup, temp); err != nil {
return "", err
}
backup = "" // 置空,防止干扰后续判断
if files, err := os.ReadDir(temp); err == nil {
if len(files) != 1 {
return "", errors.New(r.t.Get("The number of files contained in the compressed file is not 1, actual %d", len(files)))
}
if strings.HasSuffix(files[0].Name(), ".sql") {
backup = filepath.Join(temp, files[0].Name())
}
}
if backup == "" {
return "", errors.New(r.t.Get("could not find .sql backup file"))
}
return backup, nil
}
func (r *backupRepo) FixPanel() error {
if app.IsCli {
fmt.Println(r.t.Get("|-Start fixing the panel..."))
}
// 检查关键文件是否正常
flag := !io.Exists("/usr/local/etc/panel/config.yml") ||
!io.Exists(filepath.Join(app.Root, "panel", "web")) ||
!io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) ||
io.Exists("/tmp/panel-storage.zip")
// 检查数据库连接
if err := r.db.Exec("VACUUM").Error; err != nil {
flag = true
}
if err := r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
flag = true
}
if !flag {
return errors.New(r.t.Get("Files are normal and do not need to be repaired, please run panel-cli update to update the panel"))
}
// 再次确认是否需要修复
if io.Exists("/tmp/panel-storage.zip") {
// 文件齐全情况下只移除临时文件
if io.Exists(filepath.Join(app.Root, "panel", "web")) &&
io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) &&
io.Exists("/usr/local/etc/panel/config.yml") {
if err := io.Remove("/tmp/panel-storage.zip"); err != nil {
return errors.New(r.t.Get("failed to clean temporary files: %v", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Cleaned up temporary files, please run panel-cli update to update the panel"))
}
return nil
}
}
// 从备份目录中找最新的备份文件
list, err := r.List(biz.BackupTypePanel)
if err != nil {
return err
}
slices.SortFunc(list, func(a *types.BackupFile, b *types.BackupFile) int {
return int(b.Time.Unix() - a.Time.Unix())
})
if len(list) == 0 {
return errors.New(r.t.Get("No backup file found, unable to automatically repair"))
}
latest := list[0]
if app.IsCli {
fmt.Println(r.t.Get("|-Backup file used: %s", latest.Name))
}
// 解压备份文件
if app.IsCli {
fmt.Println(r.t.Get("|-Unzip backup file..."))
}
if err = io.Remove("/tmp/panel-fix"); err != nil {
return errors.New(r.t.Get("Cleaning temporary directory failed: %v", err))
}
if err = io.UnCompress(latest.Path, "/tmp/panel-fix"); err != nil {
return errors.New(r.t.Get("Unzip backup file failed: %v", err))
}
// 移动文件到对应位置
if app.IsCli {
fmt.Println(r.t.Get("|-Move backup file..."))
}
if io.Exists(filepath.Join("/tmp/panel-fix", "panel")) && io.IsDir(filepath.Join("/tmp/panel-fix", "panel")) {
if err = io.Remove(filepath.Join(app.Root, "panel")); err != nil {
return errors.New(r.t.Get("Remove panel file failed: %v", err))
}
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel"), filepath.Join(app.Root)); err != nil {
return errors.New(r.t.Get("Move panel file failed: %v", err))
}
}
if io.Exists(filepath.Join("/tmp/panel-fix", "config.yml")) {
if err = io.Mv(filepath.Join("/tmp/panel-fix", "config.yml"), "/usr/local/etc/panel/config.yml"); err != nil {
return errors.New(r.t.Get("Move panel config failed: %v", err))
}
}
if io.Exists(filepath.Join("/tmp/panel-fix", "panel-cli")) {
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel-cli"), "/usr/local/sbin/panel-cli"); err != nil {
return errors.New(r.t.Get("Move panel-cli file failed: %v", err))
}
}
// tmp 目录下如果有 storage 备份,则解压回去
if app.IsCli {
fmt.Println(r.t.Get("|-Restore panel data..."))
}
if io.Exists("/tmp/panel-storage.zip") {
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel")); err != nil {
return errors.New(r.t.Get("Unzip panel data failed: %v", err))
}
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
return errors.New(r.t.Get("Cleaning temporary file failed: %v", err))
}
}
// 下载服务文件
if !io.Exists("/etc/systemd/system/panel.service") {
if _, err = shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
return err
}
}
// 处理权限
if app.IsCli {
fmt.Println(r.t.Get("|-Set key file permissions..."))
}
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
return err
}
if err = io.Chmod("/etc/systemd/system/panel.service", 0644); err != nil {
return err
}
if err = io.Chmod("/usr/local/sbin/panel-cli", 0700); err != nil {
return err
}
if err = io.Chmod(filepath.Join(app.Root, "panel"), 0700); err != nil {
return err
}
if err = io.Remove("/tmp/panel-fix"); err != nil {
return err
}
if app.IsCli {
fmt.Println(r.t.Get("|-Fix completed"))
}
tools.RestartPanel()
return nil
}
func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
// 预先优化数据库
if err := r.db.Exec("VACUUM").Error; err != nil {
return err
}
if err := r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
return err
}
name := filepath.Base(url)
if app.IsCli {
fmt.Println(r.t.Get("|-Target version: %s", version))
fmt.Println(r.t.Get("|-Download link: %s", url))
fmt.Println(r.t.Get("|-File name: %s", name))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Downloading..."))
}
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
return errors.New(r.t.Get("Download failed: %v", err))
}
if _, err := shell.Execf("wget -T 20 -t 3 -O /tmp/%s %s", name+".sha256", checksum); err != nil {
return errors.New(r.t.Get("Download failed: %v", err))
}
if !io.Exists(filepath.Join("/tmp", name)) || !io.Exists(filepath.Join("/tmp", name+".sha256")) {
return errors.New(r.t.Get("Download file check failed"))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Verify download file..."))
}
if check, err := shell.Execf("cd /tmp && sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
return errors.New(r.t.Get("Verify download file failed: %v", err))
}
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
return errors.New(r.t.Get("|-Clean up verification file failed: %v", err))
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New(r.t.Get("Temporary file detected in /tmp, this may be caused by the last update failure, please run panel-cli fix to repair and try again"))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Backup panel data..."))
}
// 备份面板
if err := r.Create(biz.BackupTypePanel, ""); err != nil {
return errors.New(r.t.Get("|-Backup panel data failed: %v", err))
}
if err := io.Compress(filepath.Join(app.Root, "panel/storage"), nil, "/tmp/panel-storage.zip"); err != nil {
return errors.New(r.t.Get("|-Backup panel data failed: %v", err))
}
if !io.Exists("/tmp/panel-storage.zip") {
return errors.New(r.t.Get("|-Backup panel data failed, missing file"))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Cleaning old version..."))
}
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
return errors.New(r.t.Get("|-Cleaning old version failed: %v", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Unzip new version..."))
}
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel")); err != nil {
return errors.New(r.t.Get("|-Unzip new version failed: %v", err))
}
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
return errors.New(r.t.Get("|-Unzip new version failed, missing file"))
}
if err := io.Remove(filepath.Join("/tmp", name)); err != nil {
return errors.New(r.t.Get("|-Clean up temporary file failed: %v", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Restore panel data..."))
}
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel", "storage")); err != nil {
return errors.New(r.t.Get("|-Restore panel data failed: %v", err))
}
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
return errors.New(r.t.Get("|-Restore panel data failed, missing file"))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Run post-update script..."))
}
if _, err := shell.Execf("curl -sSLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
return errors.New(r.t.Get("|-Run post-update script failed: %v", err))
}
if _, err := shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
return errors.New(r.t.Get("|-Download panel service file failed: %v", err))
}
if _, err := shell.Execf("panel-cli setting write version %s", version); err != nil {
return errors.New(r.t.Get("|-Write new panel version failed: %v", err))
}
if err := io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
return errors.New(r.t.Get("|-Move panel-cli tool failed: %v", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Set key file permissions..."))
}
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
_ = io.Chmod("/etc/systemd/system/panel.service", 0644)
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
if app.IsCli {
fmt.Println(r.t.Get("|-Update completed"))
}
_, _ = shell.Execf("systemctl daemon-reload")
_ = io.Remove("/tmp/panel-storage.zip")
_ = io.Remove(filepath.Join(app.Root, "panel/config.example.yml"))
if sqlDB, err := r.db.DB(); err == nil {
_ = sqlDB.Close()
}
tools.RestartPanel()
return nil
}