Files
voidraft/internal/services/backup_service.go
landaiqing cc98e556c6
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
♻️ Optimize code
2025-11-07 22:34:12 +08:00

470 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"errors"
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/wailsapp/wails/v3/pkg/services/log"
"voidraft/internal/models"
_ "modernc.org/sqlite"
)
const (
dbSerializeFile = "voidraft_data.bin"
)
// BackupService 提供基于Git的备份功能
type BackupService struct {
configService *ConfigService
dbService *DatabaseService
repository *git.Repository
logger *log.LogService
isInitialized bool
autoBackupTicker *time.Ticker
autoBackupStop chan bool
autoBackupWg sync.WaitGroup // 等待自动备份goroutine完成
mu sync.Mutex // 推送操作互斥锁
// 配置观察者取消函数
cancelObserver CancelFunc
}
// NewBackupService 创建新的备份服务实例
func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *BackupService {
return &BackupService{
configService: configService,
dbService: dbService,
logger: logger,
}
}
func (ds *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
ds.cancelObserver = ds.configService.Watch("backup", ds.onBackupConfigChange)
if err := ds.Initialize(); err != nil {
return fmt.Errorf("initializing backup service: %w", err)
}
return nil
}
// onBackupConfigChange 备份配置变更回调
func (ds *BackupService) onBackupConfigChange(oldValue, newValue interface{}) {
// 重新加载配置
config, err := ds.configService.GetConfig()
if err != nil {
return
}
// 处理配置变更
_ = ds.HandleConfigChange(&config.Backup)
}
// Initialize 初始化备份服务
func (s *BackupService) Initialize() error {
config, repoPath, err := s.getConfigAndPath()
if err != nil {
return fmt.Errorf("getting backup config: %w", err)
}
if !config.Enabled {
return nil
}
// 初始化仓库
if err := s.initializeRepository(config, repoPath); err != nil {
return fmt.Errorf("initializing repository: %w", err)
}
// 验证远程仓库连接
if err := s.verifyRemoteConnection(config); err != nil {
return fmt.Errorf("verifying remote connection: %w", err)
}
// 启动自动备份
if config.AutoBackup && config.BackupInterval > 0 {
s.StartAutoBackup()
}
s.isInitialized = true
return nil
}
// getConfigAndPath 获取备份配置和仓库路径
func (s *BackupService) getConfigAndPath() (*models.GitBackupConfig, string, error) {
appConfig, err := s.configService.GetConfig()
if err != nil {
return nil, "", fmt.Errorf("getting app config: %w", err)
}
return &appConfig.Backup, appConfig.General.DataPath, nil
}
// initializeRepository 初始化或打开Git仓库并设置远程
func (s *BackupService) initializeRepository(config *models.GitBackupConfig, repoPath string) error {
// 检查本地仓库是否存在
_, err := os.Stat(filepath.Join(repoPath, ".git"))
if os.IsNotExist(err) {
// 仓库不存在,初始化新仓库
repo, err := git.PlainInit(repoPath, false)
if err != nil {
return fmt.Errorf("error initializing repository: %w", err)
}
s.repository = repo
} else if err != nil {
return fmt.Errorf("error checking repository path: %w", err)
} else {
// 仓库已存在,打开现有仓库
repo, err := git.PlainOpen(repoPath)
if err != nil {
return fmt.Errorf("error opening local repository: %w", err)
}
s.repository = repo
}
// 设置或更新远程仓库
remote, err := s.repository.Remote("origin")
if err != nil {
if errors.Is(err, git.ErrRemoteNotFound) {
// 远程不存在,添加远程
_, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{
Name: "origin",
URLs: []string{config.RepoURL},
})
if err != nil {
return fmt.Errorf("error creating remote: %w", err)
}
} else {
return fmt.Errorf("error getting remote: %w", err)
}
} else {
// 检查远程URL是否一致如果不一致则更新
if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != config.RepoURL {
if err := s.repository.DeleteRemote("origin"); err != nil {
return fmt.Errorf("error deleting remote: %w", err)
}
_, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{
Name: "origin",
URLs: []string{config.RepoURL},
})
if err != nil {
return fmt.Errorf("error creating new remote: %w", err)
}
}
}
return nil
}
// verifyRemoteConnection 验证远程仓库连接
func (s *BackupService) verifyRemoteConnection(config *models.GitBackupConfig) error {
auth, err := s.getAuthMethod(config)
if err != nil {
return err
}
remote, err := s.repository.Remote("origin")
if err != nil {
return err
}
_, err = remote.List(&git.ListOptions{Auth: auth})
return err
}
// getAuthMethod 根据配置获取认证方法
func (s *BackupService) getAuthMethod(config *models.GitBackupConfig) (transport.AuthMethod, error) {
switch config.AuthMethod {
case models.Token:
if config.Token == "" {
return nil, errors.New("token authentication requires a valid token")
}
return &http.BasicAuth{
Username: "git", // 使用token时用户名可以是任意值
Password: config.Token,
}, nil
case models.UserPass:
if config.Username == "" || config.Password == "" {
return nil, errors.New("username/password authentication requires both username and password")
}
return &http.BasicAuth{
Username: config.Username,
Password: config.Password,
}, nil
case models.SSHKey:
if config.SSHKeyPath == "" {
return nil, errors.New("SSH key authentication requires a valid SSH key path")
}
publicKeys, err := ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass)
if err != nil {
return nil, fmt.Errorf("error creating SSH public keys: %w", err)
}
return publicKeys, nil
default:
return nil, fmt.Errorf("unsupported authentication method: %s", config.AuthMethod)
}
}
// serializeDatabase 序列化数据库到文件
func (s *BackupService) serializeDatabase(repoPath string) error {
if s.dbService == nil || s.dbService.db == nil {
return errors.New("database service not available")
}
binFilePath := filepath.Join(repoPath, dbSerializeFile)
// 使用 VACUUM INTO 创建数据库副本,不影响现有连接
s.dbService.mu.RLock()
_, err := s.dbService.db.Exec(fmt.Sprintf("VACUUM INTO '%s'", binFilePath))
s.dbService.mu.RUnlock()
if err != nil {
return fmt.Errorf("creating database backup: %w", err)
}
return nil
}
// PushToRemote 推送本地更改到远程仓库
func (s *BackupService) PushToRemote() error {
// 互斥锁防止并发推送
s.mu.Lock()
defer s.mu.Unlock()
if !s.isInitialized {
return errors.New("backup service not initialized")
}
config, repoPath, err := s.getConfigAndPath()
if err != nil {
return fmt.Errorf("getting backup config: %w", err)
}
if !config.Enabled {
return errors.New("backup is disabled")
}
// 检查是否有未推送的commit
hasUnpushed, err := s.hasUnpushedCommits()
if err != nil {
return fmt.Errorf("checking unpushed commits: %w", err)
}
binFilePath := filepath.Join(repoPath, dbSerializeFile)
// 只有在没有未推送commit时才创建新commit
if !hasUnpushed {
// 序列化数据库
if err := s.serializeDatabase(repoPath); err != nil {
return fmt.Errorf("serializing database: %w", err)
}
// 获取工作树
w, err := s.repository.Worktree()
if err != nil {
os.Remove(binFilePath)
return fmt.Errorf("getting worktree: %w", err)
}
// 添加序列化的数据库文件
if _, err := w.Add(dbSerializeFile); err != nil {
os.Remove(binFilePath)
return fmt.Errorf("adding serialized database file: %w", err)
}
// 检查是否有变化需要提交
status, err := w.Status()
if err != nil {
os.Remove(binFilePath)
return fmt.Errorf("getting worktree status: %w", err)
}
// 如果没有变化,删除文件并返回
if status.IsClean() {
os.Remove(binFilePath)
return errors.New("no changes to backup")
}
// 创建提交
_, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{
Author: &object.Signature{
Name: "voidraft",
Email: "backup@voidraft.app",
When: time.Now(),
},
})
if err != nil {
os.Remove(binFilePath)
if strings.Contains(err.Error(), "cannot create empty commit") {
return errors.New("no changes to backup")
}
return fmt.Errorf("creating commit: %w", err)
}
}
// 获取认证方法并推送到远程
auth, err := s.getAuthMethod(config)
if err != nil {
return fmt.Errorf("getting auth method: %w", err)
}
// 推送到远程仓库包括之前失败的commit
if err := s.repository.Push(&git.PushOptions{
RemoteName: "origin",
Auth: auth,
}); err != nil {
return err
}
// 只在推送成功后删除临时文件
os.Remove(binFilePath)
return nil
}
// hasUnpushedCommits 检查是否有未推送的commit
func (s *BackupService) hasUnpushedCommits() (bool, error) {
localRef, err := s.repository.Head()
if err != nil {
return false, nil
}
config, _, err := s.getConfigAndPath()
if err != nil {
return false, err
}
auth, err := s.getAuthMethod(config)
if err != nil {
return false, err
}
remote, err := s.repository.Remote("origin")
if err != nil {
return false, err
}
refs, err := remote.List(&git.ListOptions{Auth: auth})
if err != nil {
return false, err
}
localHash := localRef.Hash()
for _, ref := range refs {
if ref.Name() == localRef.Name() {
return localHash != ref.Hash(), nil
}
}
return true, nil
}
// StartAutoBackup 启动自动备份定时器
func (s *BackupService) StartAutoBackup() error {
config, _, err := s.getConfigAndPath()
if err != nil {
return fmt.Errorf("getting backup config: %w", err)
}
if !config.AutoBackup || config.BackupInterval <= 0 {
return nil
}
s.StopAutoBackup()
// 将秒转换为分钟
s.autoBackupTicker = time.NewTicker(time.Duration(config.BackupInterval) * time.Minute)
s.autoBackupStop = make(chan bool)
s.autoBackupWg.Add(1)
go func() {
defer s.autoBackupWg.Done()
for {
select {
case <-s.autoBackupTicker.C:
s.PushToRemote()
case <-s.autoBackupStop:
return
}
}
}()
return nil
}
// StopAutoBackup 停止自动备份
func (s *BackupService) StopAutoBackup() {
if s.autoBackupTicker != nil {
s.autoBackupTicker.Stop()
s.autoBackupTicker = nil
}
if s.autoBackupStop != nil {
close(s.autoBackupStop)
s.autoBackupStop = nil
s.autoBackupWg.Wait()
}
}
// Reinitialize 重新初始化备份服务,用于响应配置变更
func (s *BackupService) Reinitialize() error {
// 先停止自动备份等待goroutine完成
s.StopAutoBackup()
s.mu.Lock()
s.isInitialized = false
s.mu.Unlock()
// 重新初始化
return s.Initialize()
}
// HandleConfigChange 处理备份配置变更
func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error {
// 如果备份功能禁用,只需停止自动备份
if !config.Enabled {
s.StopAutoBackup()
s.mu.Lock()
s.isInitialized = false
s.mu.Unlock()
return nil
}
// 如果服务已初始化,重新初始化以应用新配置
if s.isInitialized {
return s.Reinitialize()
}
// 如果服务未初始化但已启用,则初始化
if config.Enabled && !s.isInitialized {
return s.Initialize()
}
return nil
}
// ServiceShutdown 服务关闭时的清理工作
func (s *BackupService) ServiceShutdown() {
// 取消配置观察者
if s.cancelObserver != nil {
s.cancelObserver()
}
s.StopAutoBackup()
}