feat: 实现后台在线更新功能
- 前端添加更新和重启按钮,支持一键更新 Release 构建 - 修复条件判断优先级问题,确保错误/成功状态正确显示 - 后端使用原子文件替换模式,确保更新过程安全可靠 - 在可执行文件同目录创建临时文件,保证 rename 原子性 - 删除未使用的 copyFile 函数,保持代码整洁
This commit is contained in:
@@ -125,6 +125,7 @@ func (s *UpdateService) CheckUpdate(ctx context.Context, force bool) (*UpdateInf
|
||||
}
|
||||
|
||||
// PerformUpdate downloads and applies the update
|
||||
// Uses atomic file replacement pattern for safe in-place updates
|
||||
func (s *UpdateService) PerformUpdate(ctx context.Context) error {
|
||||
info, err := s.CheckUpdate(ctx, true)
|
||||
if err != nil {
|
||||
@@ -173,8 +174,11 @@ func (s *UpdateService) PerformUpdate(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to resolve symlinks: %w", err)
|
||||
}
|
||||
|
||||
// Create temp directory for extraction
|
||||
tempDir, err := os.MkdirTemp("", "sub2api-update-*")
|
||||
exeDir := filepath.Dir(exePath)
|
||||
|
||||
// Create temp directory in the SAME directory as executable
|
||||
// This ensures os.Rename is atomic (same filesystem)
|
||||
tempDir, err := os.MkdirTemp(exeDir, ".sub2api-update-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
@@ -199,23 +203,36 @@ func (s *UpdateService) PerformUpdate(ctx context.Context) error {
|
||||
return fmt.Errorf("extraction failed: %w", err)
|
||||
}
|
||||
|
||||
// Backup current binary
|
||||
backupFile := exePath + ".backup"
|
||||
if err := os.Rename(exePath, backupFile); err != nil {
|
||||
return fmt.Errorf("backup failed: %w", err)
|
||||
}
|
||||
|
||||
// Replace with new binary
|
||||
if err := copyFile(newBinaryPath, exePath); err != nil {
|
||||
os.Rename(backupFile, exePath)
|
||||
return fmt.Errorf("replace failed: %w", err)
|
||||
}
|
||||
|
||||
// Make executable
|
||||
if err := os.Chmod(exePath, 0755); err != nil {
|
||||
// Set executable permission before replacement
|
||||
if err := os.Chmod(newBinaryPath, 0755); err != nil {
|
||||
return fmt.Errorf("chmod failed: %w", err)
|
||||
}
|
||||
|
||||
// Atomic replacement using rename pattern:
|
||||
// 1. Rename current -> backup (atomic on Unix)
|
||||
// 2. Rename new -> current (atomic on Unix, same filesystem)
|
||||
// If step 2 fails, restore backup
|
||||
backupPath := exePath + ".backup"
|
||||
|
||||
// Remove old backup if exists
|
||||
os.Remove(backupPath)
|
||||
|
||||
// Step 1: Move current binary to backup
|
||||
if err := os.Rename(exePath, backupPath); err != nil {
|
||||
return fmt.Errorf("backup failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Move new binary to target location (atomic, same filesystem)
|
||||
if err := os.Rename(newBinaryPath, exePath); err != nil {
|
||||
// Restore backup on failure
|
||||
if restoreErr := os.Rename(backupPath, exePath); restoreErr != nil {
|
||||
return fmt.Errorf("replace failed and restore failed: %w (restore error: %v)", err, restoreErr)
|
||||
}
|
||||
return fmt.Errorf("replace failed (restored backup): %w", err)
|
||||
}
|
||||
|
||||
// Success - backup file is kept for rollback capability
|
||||
// It will be cleaned up on next successful update
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -515,23 +532,6 @@ func (s *UpdateService) extractBinary(archivePath, destPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UpdateService) getFromCache(ctx context.Context) (*UpdateInfo, error) {
|
||||
data, err := s.rdb.Get(ctx, updateCacheKey).Result()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user