635 lines
15 KiB
Go
635 lines
15 KiB
Go
|
|
package service
|
||
|
|
|
||
|
|
import (
|
||
|
|
"archive/tar"
|
||
|
|
"compress/gzip"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"seeyon-filesystem/config"
|
||
|
|
"seeyon-filesystem/dto"
|
||
|
|
"seeyon-filesystem/model"
|
||
|
|
)
|
||
|
|
|
||
|
|
// BackupService 备份服务
|
||
|
|
type BackupService struct{}
|
||
|
|
|
||
|
|
func NewBackupService() *BackupService {
|
||
|
|
return &BackupService{}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 策略管理 ==========
|
||
|
|
|
||
|
|
// CreatePolicy 创建备份策略
|
||
|
|
func (s *BackupService) CreatePolicy(req *dto.BackupPolicyCreateRequest) error {
|
||
|
|
targetConfig := req.TargetConfig
|
||
|
|
if targetConfig == "" {
|
||
|
|
targetConfig = "{}"
|
||
|
|
}
|
||
|
|
|
||
|
|
policy := &model.BackupPolicy{
|
||
|
|
Name: req.Name,
|
||
|
|
BackupType: req.BackupType,
|
||
|
|
TargetType: req.TargetType,
|
||
|
|
TargetConfig: targetConfig,
|
||
|
|
IncludeDB: req.IncludeDB,
|
||
|
|
IncludeFiles: req.IncludeFiles,
|
||
|
|
CronExpr: req.CronExpr,
|
||
|
|
MaxBackups: req.MaxBackups,
|
||
|
|
Status: 1,
|
||
|
|
}
|
||
|
|
if policy.MaxBackups <= 0 {
|
||
|
|
policy.MaxBackups = 7
|
||
|
|
}
|
||
|
|
return config.DB.Create(policy).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListPolicies 获取所有备份策略
|
||
|
|
func (s *BackupService) ListPolicies() ([]dto.BackupPolicyVO, error) {
|
||
|
|
var policies []model.BackupPolicy
|
||
|
|
if err := config.DB.Order("created_at DESC").Find(&policies).Error; err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
vos := make([]dto.BackupPolicyVO, 0, len(policies))
|
||
|
|
for _, p := range policies {
|
||
|
|
vo := dto.BackupPolicyVO{
|
||
|
|
ID: p.ID,
|
||
|
|
Name: p.Name,
|
||
|
|
BackupType: p.BackupType,
|
||
|
|
TargetType: p.TargetType,
|
||
|
|
TargetConfig: p.TargetConfig,
|
||
|
|
IncludeDB: p.IncludeDB,
|
||
|
|
IncludeFiles: p.IncludeFiles,
|
||
|
|
CronExpr: p.CronExpr,
|
||
|
|
MaxBackups: p.MaxBackups,
|
||
|
|
Status: p.Status,
|
||
|
|
CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
|
|
}
|
||
|
|
if p.LastRunAt != nil {
|
||
|
|
t := p.LastRunAt.Format("2006-01-02 15:04:05")
|
||
|
|
vo.LastRunAt = &t
|
||
|
|
}
|
||
|
|
if p.NextRunAt != nil {
|
||
|
|
t := p.NextRunAt.Format("2006-01-02 15:04:05")
|
||
|
|
vo.NextRunAt = &t
|
||
|
|
}
|
||
|
|
vos = append(vos, vo)
|
||
|
|
}
|
||
|
|
return vos, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeletePolicy 删除备份策略
|
||
|
|
func (s *BackupService) DeletePolicy(id uint64) error {
|
||
|
|
return config.DB.Delete(&model.BackupPolicy{}, id).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// TogglePolicy 启用/禁用备份策略
|
||
|
|
func (s *BackupService) TogglePolicy(id uint64, status int8) error {
|
||
|
|
return config.DB.Model(&model.BackupPolicy{}).Where("id = ?", id).Update("status", status).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 执行备份 ==========
|
||
|
|
|
||
|
|
// ExecuteBackup 手动执行备份
|
||
|
|
func (s *BackupService) ExecuteBackup(policyID uint64) (*model.BackupLog, error) {
|
||
|
|
var policy model.BackupPolicy
|
||
|
|
if err := config.DB.First(&policy, policyID).Error; err != nil {
|
||
|
|
return nil, fmt.Errorf("备份策略不存在")
|
||
|
|
}
|
||
|
|
|
||
|
|
// 创建备份日志
|
||
|
|
log := &model.BackupLog{
|
||
|
|
PolicyID: policyID,
|
||
|
|
PolicyName: policy.Name,
|
||
|
|
BackupType: policy.BackupType,
|
||
|
|
Status: "running",
|
||
|
|
StartedAt: time.Now(),
|
||
|
|
}
|
||
|
|
config.DB.Create(log)
|
||
|
|
|
||
|
|
// 异步执行备份
|
||
|
|
go s.doBackup(&policy, log)
|
||
|
|
|
||
|
|
return log, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// doBackup 执行实际备份
|
||
|
|
func (s *BackupService) doBackup(policy *model.BackupPolicy, log *model.BackupLog) {
|
||
|
|
startTime := time.Now()
|
||
|
|
|
||
|
|
// 创建备份目录
|
||
|
|
backupDir := filepath.Join(config.AppConfig.Storage.LocalBasePath, ".backups")
|
||
|
|
os.MkdirAll(backupDir, 0755)
|
||
|
|
|
||
|
|
timestamp := time.Now().Format("20060102_150405")
|
||
|
|
backupFileName := fmt.Sprintf("backup_%s_%s.tar.gz", policy.BackupType, timestamp)
|
||
|
|
backupPath := filepath.Join(backupDir, backupFileName)
|
||
|
|
|
||
|
|
// 创建tar.gz文件
|
||
|
|
tarFile, err := os.Create(backupPath)
|
||
|
|
if err != nil {
|
||
|
|
s.finishLog(log, "failed", "", 0, 0, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer tarFile.Close()
|
||
|
|
|
||
|
|
gzWriter := gzip.NewWriter(tarFile)
|
||
|
|
defer gzWriter.Close()
|
||
|
|
|
||
|
|
tarWriter := tar.NewWriter(gzWriter)
|
||
|
|
defer tarWriter.Close()
|
||
|
|
|
||
|
|
totalSize := int64(0)
|
||
|
|
filesCount := 0
|
||
|
|
|
||
|
|
// 备份数据库
|
||
|
|
if policy.IncludeDB == 1 {
|
||
|
|
dbSize, err := s.backupDatabase(tarWriter, timestamp)
|
||
|
|
if err != nil {
|
||
|
|
s.finishLog(log, "failed", "", 0, 0, "数据库备份失败: "+err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
totalSize += dbSize
|
||
|
|
}
|
||
|
|
|
||
|
|
// 备份文件
|
||
|
|
if policy.IncludeFiles == 1 {
|
||
|
|
fileSize, count, err := s.backupFiles(tarWriter)
|
||
|
|
if err != nil {
|
||
|
|
s.finishLog(log, "failed", "", 0, 0, "文件备份失败: "+err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
totalSize += fileSize
|
||
|
|
filesCount = count
|
||
|
|
}
|
||
|
|
|
||
|
|
tarWriter.Close()
|
||
|
|
gzWriter.Close()
|
||
|
|
tarFile.Close()
|
||
|
|
|
||
|
|
// 获取备份文件大小
|
||
|
|
backupInfo, _ := os.Stat(backupPath)
|
||
|
|
backupSize := int64(0)
|
||
|
|
if backupInfo != nil {
|
||
|
|
backupSize = backupInfo.Size()
|
||
|
|
}
|
||
|
|
|
||
|
|
duration := int(time.Since(startTime).Seconds())
|
||
|
|
|
||
|
|
// 更新策略最后执行时间
|
||
|
|
now := time.Now()
|
||
|
|
config.DB.Model(policy).Updates(map[string]interface{}{
|
||
|
|
"last_run_at": now,
|
||
|
|
})
|
||
|
|
|
||
|
|
s.finishLog(log, "success", backupPath, backupSize, filesCount, "")
|
||
|
|
log.DBSize = totalSize
|
||
|
|
log.Duration = duration
|
||
|
|
config.DB.Save(log)
|
||
|
|
|
||
|
|
// 清理旧备份
|
||
|
|
s.cleanOldBackups(policy)
|
||
|
|
}
|
||
|
|
|
||
|
|
// backupDatabase 备份数据库
|
||
|
|
func (s *BackupService) backupDatabase(tarWriter *tar.Writer, timestamp string) (int64, error) {
|
||
|
|
dbCfg := config.AppConfig.Database
|
||
|
|
dumpFile := filepath.Join(os.TempDir(), fmt.Sprintf("db_dump_%s.sql", timestamp))
|
||
|
|
|
||
|
|
mysqldumpPath := s.findTool("mysqldump", config.AppConfig.Backup.MysqlDumpPath)
|
||
|
|
|
||
|
|
cmd := exec.Command(mysqldumpPath,
|
||
|
|
"-h", dbCfg.Host,
|
||
|
|
"-P", fmt.Sprintf("%d", dbCfg.Port),
|
||
|
|
"-u", dbCfg.Username,
|
||
|
|
"-p"+dbCfg.Password,
|
||
|
|
"--single-transaction",
|
||
|
|
"--routines",
|
||
|
|
"--triggers",
|
||
|
|
dbCfg.DBName,
|
||
|
|
)
|
||
|
|
|
||
|
|
outFile, err := os.Create(dumpFile)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
cmd.Stdout = outFile
|
||
|
|
cmd.Stderr = os.Stderr
|
||
|
|
|
||
|
|
if err := cmd.Run(); err != nil {
|
||
|
|
outFile.Close()
|
||
|
|
os.Remove(dumpFile)
|
||
|
|
return 0, fmt.Errorf("mysqldump执行失败: %w", err)
|
||
|
|
}
|
||
|
|
outFile.Close()
|
||
|
|
|
||
|
|
// 获取文件大小
|
||
|
|
info, _ := os.Stat(dumpFile)
|
||
|
|
dbSize := int64(0)
|
||
|
|
if info != nil {
|
||
|
|
dbSize = info.Size()
|
||
|
|
}
|
||
|
|
|
||
|
|
// 写入tar
|
||
|
|
file, err := os.Open(dumpFile)
|
||
|
|
if err != nil {
|
||
|
|
os.Remove(dumpFile)
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
header := &tar.Header{
|
||
|
|
Name: "database/" + dbCfg.DBName + ".sql",
|
||
|
|
Size: dbSize,
|
||
|
|
Mode: 0644,
|
||
|
|
ModTime: time.Now(),
|
||
|
|
}
|
||
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
||
|
|
os.Remove(dumpFile)
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := io.Copy(tarWriter, file); err != nil {
|
||
|
|
os.Remove(dumpFile)
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
|
||
|
|
os.Remove(dumpFile)
|
||
|
|
return dbSize, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// backupFiles 备份文件
|
||
|
|
func (s *BackupService) backupFiles(tarWriter *tar.Writer) (int64, int, error) {
|
||
|
|
basePath := config.AppConfig.Storage.LocalBasePath
|
||
|
|
totalSize := int64(0)
|
||
|
|
count := 0
|
||
|
|
|
||
|
|
err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
|
||
|
|
if err != nil {
|
||
|
|
return nil // 跳过错误
|
||
|
|
}
|
||
|
|
// 跳过备份目录和临时目录
|
||
|
|
if info.IsDir() && (info.Name() == ".backups" || info.Name() == ".tmp") {
|
||
|
|
return filepath.SkipDir
|
||
|
|
}
|
||
|
|
if info.IsDir() {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
relPath, _ := filepath.Rel(basePath, path)
|
||
|
|
|
||
|
|
file, err := os.Open(path)
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
header := &tar.Header{
|
||
|
|
Name: "files/" + relPath,
|
||
|
|
Size: info.Size(),
|
||
|
|
Mode: int64(info.Mode()),
|
||
|
|
ModTime: info.ModTime(),
|
||
|
|
}
|
||
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := io.Copy(tarWriter, file); err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
totalSize += info.Size()
|
||
|
|
count++
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
|
||
|
|
return totalSize, count, err
|
||
|
|
}
|
||
|
|
|
||
|
|
// finishLog 更新备份日志状态
|
||
|
|
func (s *BackupService) finishLog(log *model.BackupLog, status, filePath string, fileSize int64, filesCount int, errMsg string) {
|
||
|
|
now := time.Now()
|
||
|
|
log.Status = status
|
||
|
|
log.FilePath = filePath
|
||
|
|
log.FileSize = fileSize
|
||
|
|
log.FilesCount = filesCount
|
||
|
|
log.FinishedAt = &now
|
||
|
|
log.ErrorMsg = errMsg
|
||
|
|
if !now.IsZero() && !log.StartedAt.IsZero() {
|
||
|
|
log.Duration = int(now.Sub(log.StartedAt).Seconds())
|
||
|
|
}
|
||
|
|
config.DB.Save(log)
|
||
|
|
}
|
||
|
|
|
||
|
|
// cleanOldBackups 清理旧备份
|
||
|
|
func (s *BackupService) cleanOldBackups(policy *model.BackupPolicy) {
|
||
|
|
var logs []model.BackupLog
|
||
|
|
config.DB.Where("policy_id = ? AND status = 'success'", policy.ID).
|
||
|
|
Order("started_at DESC").Offset(policy.MaxBackups).Find(&logs)
|
||
|
|
|
||
|
|
for _, log := range logs {
|
||
|
|
// 删除备份文件
|
||
|
|
if log.FilePath != "" {
|
||
|
|
os.Remove(log.FilePath)
|
||
|
|
}
|
||
|
|
config.DB.Delete(&log)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 查询 ==========
|
||
|
|
|
||
|
|
// ListLogs 查询备份日志
|
||
|
|
func (s *BackupService) ListLogs(policyID uint64, page, size int) ([]dto.BackupLogVO, int64, error) {
|
||
|
|
query := config.DB.Model(&model.BackupLog{})
|
||
|
|
if policyID > 0 {
|
||
|
|
query = query.Where("policy_id = ?", policyID)
|
||
|
|
}
|
||
|
|
|
||
|
|
var total int64
|
||
|
|
query.Count(&total)
|
||
|
|
|
||
|
|
var logs []model.BackupLog
|
||
|
|
query.Order("started_at DESC").Offset((page - 1) * size).Limit(size).Find(&logs)
|
||
|
|
|
||
|
|
vos := make([]dto.BackupLogVO, 0, len(logs))
|
||
|
|
for _, l := range logs {
|
||
|
|
vo := dto.BackupLogVO{
|
||
|
|
ID: l.ID,
|
||
|
|
PolicyID: l.PolicyID,
|
||
|
|
PolicyName: l.PolicyName,
|
||
|
|
BackupType: l.BackupType,
|
||
|
|
Status: l.Status,
|
||
|
|
FilePath: l.FilePath,
|
||
|
|
FileSize: l.FileSize,
|
||
|
|
DBSize: l.DBSize,
|
||
|
|
FilesCount: l.FilesCount,
|
||
|
|
Duration: l.Duration,
|
||
|
|
ErrorMsg: l.ErrorMsg,
|
||
|
|
StartedAt: l.StartedAt.Format("2006-01-02 15:04:05"),
|
||
|
|
}
|
||
|
|
if l.FinishedAt != nil {
|
||
|
|
t := l.FinishedAt.Format("2006-01-02 15:04:05")
|
||
|
|
vo.FinishedAt = &t
|
||
|
|
}
|
||
|
|
vos = append(vos, vo)
|
||
|
|
}
|
||
|
|
return vos, total, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 恢复 ==========
|
||
|
|
|
||
|
|
// RestoreResult 恢复结果
|
||
|
|
type RestoreResult struct {
|
||
|
|
Success bool `json:"success"`
|
||
|
|
Message string `json:"message"`
|
||
|
|
DBRestored bool `json:"db_restored"`
|
||
|
|
FilesCount int `json:"files_count"`
|
||
|
|
Duration int `json:"duration_ms"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// RestoreFromBackup 从备份文件恢复
|
||
|
|
// logID: 备份日志ID
|
||
|
|
// restoreDB: 是否恢复数据库
|
||
|
|
// restoreFiles: 是否恢复文件
|
||
|
|
func (s *BackupService) RestoreFromBackup(logID uint64, restoreDB, restoreFiles bool) (*RestoreResult, error) {
|
||
|
|
// 获取备份日志
|
||
|
|
var log model.BackupLog
|
||
|
|
if err := config.DB.First(&log, logID).Error; err != nil {
|
||
|
|
return nil, fmt.Errorf("备份记录不存在")
|
||
|
|
}
|
||
|
|
if log.Status != "success" {
|
||
|
|
return nil, fmt.Errorf("只能恢复成功的备份")
|
||
|
|
}
|
||
|
|
if log.FilePath == "" {
|
||
|
|
return nil, fmt.Errorf("备份文件路径为空")
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查备份文件是否存在
|
||
|
|
if _, err := os.Stat(log.FilePath); os.IsNotExist(err) {
|
||
|
|
return nil, fmt.Errorf("备份文件不存在: %s", log.FilePath)
|
||
|
|
}
|
||
|
|
|
||
|
|
startTime := time.Now()
|
||
|
|
result := &RestoreResult{}
|
||
|
|
|
||
|
|
// 解压备份文件
|
||
|
|
tempDir := filepath.Join(os.TempDir(), "restore_"+fmt.Sprintf("%d", logID))
|
||
|
|
os.MkdirAll(tempDir, 0755)
|
||
|
|
defer os.RemoveAll(tempDir)
|
||
|
|
|
||
|
|
if err := s.extractTarGz(log.FilePath, tempDir); err != nil {
|
||
|
|
return nil, fmt.Errorf("解压备份文件失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 恢复数据库
|
||
|
|
if restoreDB {
|
||
|
|
dbFile := filepath.Join(tempDir, "database")
|
||
|
|
if _, err := os.Stat(dbFile); err == nil {
|
||
|
|
// 找到SQL文件
|
||
|
|
entries, _ := os.ReadDir(dbFile)
|
||
|
|
for _, entry := range entries {
|
||
|
|
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".sql" {
|
||
|
|
sqlPath := filepath.Join(dbFile, entry.Name())
|
||
|
|
if err := s.restoreDatabase(sqlPath); err != nil {
|
||
|
|
result.Message = "数据库恢复失败: " + err.Error()
|
||
|
|
result.Duration = int(time.Since(startTime).Milliseconds())
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
result.DBRestored = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 恢复文件
|
||
|
|
if restoreFiles {
|
||
|
|
filesDir := filepath.Join(tempDir, "files")
|
||
|
|
if _, err := os.Stat(filesDir); err == nil {
|
||
|
|
basePath := config.AppConfig.Storage.LocalBasePath
|
||
|
|
count, err := s.restoreFiles(filesDir, basePath)
|
||
|
|
if err != nil {
|
||
|
|
result.Message = "文件恢复失败: " + err.Error()
|
||
|
|
result.Duration = int(time.Since(startTime).Milliseconds())
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
result.FilesCount = count
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
result.Success = true
|
||
|
|
result.Duration = int(time.Since(startTime).Milliseconds())
|
||
|
|
|
||
|
|
parts := []string{}
|
||
|
|
if result.DBRestored {
|
||
|
|
parts = append(parts, "数据库已恢复")
|
||
|
|
}
|
||
|
|
if result.FilesCount > 0 {
|
||
|
|
parts = append(parts, fmt.Sprintf("%d个文件已恢复", result.FilesCount))
|
||
|
|
}
|
||
|
|
if len(parts) == 0 {
|
||
|
|
result.Message = "没有可恢复的内容"
|
||
|
|
} else {
|
||
|
|
result.Message = "恢复成功: " + strings.Join(parts, ", ")
|
||
|
|
}
|
||
|
|
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// extractTarGz 解压tar.gz文件
|
||
|
|
func (s *BackupService) extractTarGz(tarGzPath, destDir string) error {
|
||
|
|
file, err := os.Open(tarGzPath)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
gzReader, err := gzip.NewReader(file)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer gzReader.Close()
|
||
|
|
|
||
|
|
tarReader := tar.NewReader(gzReader)
|
||
|
|
for {
|
||
|
|
header, err := tarReader.Next()
|
||
|
|
if err == io.EOF {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
targetPath := filepath.Join(destDir, header.Name)
|
||
|
|
|
||
|
|
switch header.Typeflag {
|
||
|
|
case tar.TypeDir:
|
||
|
|
os.MkdirAll(targetPath, 0755)
|
||
|
|
case tar.TypeReg:
|
||
|
|
os.MkdirAll(filepath.Dir(targetPath), 0755)
|
||
|
|
outFile, err := os.Create(targetPath)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
io.Copy(outFile, tarReader)
|
||
|
|
outFile.Close()
|
||
|
|
os.Chmod(targetPath, os.FileMode(header.Mode))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// restoreDatabase 从SQL文件恢复数据库
|
||
|
|
func (s *BackupService) restoreDatabase(sqlFilePath string) error {
|
||
|
|
dbCfg := config.AppConfig.Database
|
||
|
|
|
||
|
|
mysqlPath := s.findTool("mysql", config.AppConfig.Backup.MysqlPath)
|
||
|
|
|
||
|
|
cmd := exec.Command(mysqlPath,
|
||
|
|
"-h", dbCfg.Host,
|
||
|
|
"-P", fmt.Sprintf("%d", dbCfg.Port),
|
||
|
|
"-u", dbCfg.Username,
|
||
|
|
"-p"+dbCfg.Password,
|
||
|
|
dbCfg.DBName,
|
||
|
|
)
|
||
|
|
|
||
|
|
sqlFile, err := os.Open(sqlFilePath)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer sqlFile.Close()
|
||
|
|
|
||
|
|
cmd.Stdin = sqlFile
|
||
|
|
cmd.Stderr = os.Stderr
|
||
|
|
|
||
|
|
return cmd.Run()
|
||
|
|
}
|
||
|
|
|
||
|
|
// restoreFiles 恢复文件到目标目录
|
||
|
|
func (s *BackupService) restoreFiles(srcDir, destDir string) (int, error) {
|
||
|
|
count := 0
|
||
|
|
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
relPath, _ := filepath.Rel(srcDir, path)
|
||
|
|
targetPath := filepath.Join(destDir, relPath)
|
||
|
|
|
||
|
|
if info.IsDir() {
|
||
|
|
os.MkdirAll(targetPath, 0755)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// 复制文件
|
||
|
|
srcFile, err := os.Open(path)
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
defer srcFile.Close()
|
||
|
|
|
||
|
|
os.MkdirAll(filepath.Dir(targetPath), 0755)
|
||
|
|
dstFile, err := os.Create(targetPath)
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
defer dstFile.Close()
|
||
|
|
|
||
|
|
io.Copy(dstFile, srcFile)
|
||
|
|
count++
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
|
||
|
|
return count, err
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListBackups 列出所有可恢复的备份文件
|
||
|
|
func (s *BackupService) ListBackups() ([]dto.BackupLogVO, error) {
|
||
|
|
var logs []model.BackupLog
|
||
|
|
config.DB.Where("status = 'success' AND file_path != ''").
|
||
|
|
Order("started_at DESC").Find(&logs)
|
||
|
|
|
||
|
|
vos := make([]dto.BackupLogVO, 0, len(logs))
|
||
|
|
for _, l := range logs {
|
||
|
|
vo := dto.BackupLogVO{
|
||
|
|
ID: l.ID,
|
||
|
|
PolicyName: l.PolicyName,
|
||
|
|
BackupType: l.BackupType,
|
||
|
|
FileSize: l.FileSize,
|
||
|
|
FilesCount: l.FilesCount,
|
||
|
|
StartedAt: l.StartedAt.Format("2006-01-02 15:04:05"),
|
||
|
|
}
|
||
|
|
vos = append(vos, vo)
|
||
|
|
}
|
||
|
|
return vos, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// findTool 查找可执行工具路径
|
||
|
|
// 优先使用配置路径, 否则搜索常见路径
|
||
|
|
func (s *BackupService) findTool(name, configPath string) string {
|
||
|
|
if configPath != "" {
|
||
|
|
if _, err := os.Stat(configPath); err == nil {
|
||
|
|
return configPath
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := exec.LookPath(name); err == nil {
|
||
|
|
return name
|
||
|
|
}
|
||
|
|
commonPaths := []string{
|
||
|
|
fmt.Sprintf(`C:\Program Files\MySQL\MySQL Server 8.0\bin\%s.exe`, name),
|
||
|
|
fmt.Sprintf(`/usr/bin/%s`, name),
|
||
|
|
}
|
||
|
|
for _, p := range commonPaths {
|
||
|
|
if _, err := os.Stat(p); err == nil {
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return name
|
||
|
|
}
|