135 lines
3.4 KiB
Go
135 lines
3.4 KiB
Go
package installer
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
|
||
"skillmgr/pkg/fileutil"
|
||
)
|
||
|
||
// Transaction 事务性安装
|
||
type Transaction struct {
|
||
stagingDir string
|
||
targetDir string
|
||
fileMap map[string]string // source → dest
|
||
}
|
||
|
||
// NewTransaction 创建事务
|
||
// 在系统临时目录创建 staging 目录
|
||
func NewTransaction(targetDir string, fileMap map[string]string) (*Transaction, error) {
|
||
// 在系统临时目录创建 staging 目录
|
||
stagingDir, err := os.MkdirTemp("", "skillmgr-*")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("创建 staging 目录失败: %w", err)
|
||
}
|
||
|
||
return &Transaction{
|
||
stagingDir: stagingDir,
|
||
targetDir: targetDir,
|
||
fileMap: fileMap,
|
||
}, nil
|
||
}
|
||
|
||
// Stage 阶段:复制文件到 staging 目录
|
||
func (t *Transaction) Stage() error {
|
||
stagedCount := 0
|
||
|
||
for src, dest := range t.fileMap {
|
||
// 计算相对于 targetDir 的路径
|
||
relPath, err := filepath.Rel(t.targetDir, dest)
|
||
if err != nil {
|
||
return fmt.Errorf("计算相对路径失败: %w", err)
|
||
}
|
||
|
||
stagingDest := filepath.Join(t.stagingDir, relPath)
|
||
|
||
// 确保目标目录存在
|
||
if err := os.MkdirAll(filepath.Dir(stagingDest), 0755); err != nil {
|
||
return fmt.Errorf("创建 staging 子目录失败: %w", err)
|
||
}
|
||
|
||
// 复制文件
|
||
if err := fileutil.CopyFile(src, stagingDest); err != nil {
|
||
return fmt.Errorf("复制文件到 staging 失败: %w", err)
|
||
}
|
||
stagedCount++
|
||
}
|
||
|
||
// 验证 staging 完整性:检查文件数量是否与预期一致
|
||
if err := t.verifyStagingIntegrity(stagedCount); err != nil {
|
||
return fmt.Errorf("staging 验证失败: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// verifyStagingIntegrity 验证 staging 目录中的文件数量
|
||
func (t *Transaction) verifyStagingIntegrity(expectedCount int) error {
|
||
actualCount := 0
|
||
|
||
err := filepath.Walk(t.stagingDir, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !info.IsDir() {
|
||
actualCount++
|
||
}
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("遍历 staging 目录失败: %w", err)
|
||
}
|
||
|
||
if actualCount != expectedCount {
|
||
return fmt.Errorf("文件数量不匹配: 预期 %d 个文件,实际 %d 个", expectedCount, actualCount)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Commit 提交:将 staging 目录移动到目标位置
|
||
func (t *Transaction) Commit() error {
|
||
// 确保目标目录的父目录存在
|
||
if err := os.MkdirAll(filepath.Dir(t.targetDir), 0755); err != nil {
|
||
return fmt.Errorf("创建目标父目录失败: %w", err)
|
||
}
|
||
|
||
// 如果目标目录已存在,先删除(已经过用户确认)
|
||
if _, err := os.Stat(t.targetDir); err == nil {
|
||
if err := os.RemoveAll(t.targetDir); err != nil {
|
||
return fmt.Errorf("删除已存在的目标目录失败: %w", err)
|
||
}
|
||
}
|
||
|
||
// 尝试原子性移动 staging 目录到目标位置
|
||
if err := os.Rename(t.stagingDir, t.targetDir); err != nil {
|
||
// 如果跨文件系统,Rename 会失败,改用复制
|
||
// 使用 defer 确保 staging 目录被清理
|
||
defer os.RemoveAll(t.stagingDir)
|
||
if err := fileutil.CopyDir(t.stagingDir, t.targetDir); err != nil {
|
||
return fmt.Errorf("复制 staging 到目标失败: %w", err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Rollback 回滚:清理 staging 目录
|
||
func (t *Transaction) Rollback() {
|
||
if t.stagingDir != "" {
|
||
os.RemoveAll(t.stagingDir)
|
||
}
|
||
}
|
||
|
||
// StagingDir 获取 staging 目录路径
|
||
func (t *Transaction) StagingDir() string {
|
||
return t.stagingDir
|
||
}
|
||
|
||
// TargetDir 获取目标目录路径
|
||
func (t *Transaction) TargetDir() string {
|
||
return t.targetDir
|
||
}
|