Files
voidraft/internal/services/migration_service.go
2025-06-22 15:08:38 +08:00

425 lines
9.2 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 (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/wailsapp/wails/v3/pkg/services/log"
)
// MigrationStatus 迁移状态
type MigrationStatus string
const (
MigrationStatusMigrating MigrationStatus = "migrating"
MigrationStatusCompleted MigrationStatus = "completed"
MigrationStatusFailed MigrationStatus = "failed"
)
// MigrationProgress 迁移进度信息
type MigrationProgress struct {
Status MigrationStatus `json:"status"`
Progress float64 `json:"progress"`
Error string `json:"error,omitempty"`
}
// MigrationService 迁移服务
type MigrationService struct {
logger *log.LoggerService
mu sync.RWMutex
progress atomic.Value // stores MigrationProgress
ctx context.Context
cancel context.CancelFunc
}
// NewMigrationService 创建迁移服务
func NewMigrationService(logger *log.LoggerService) *MigrationService {
if logger == nil {
logger = log.New()
}
ms := &MigrationService{
logger: logger,
}
// 初始化进度
ms.progress.Store(MigrationProgress{
Status: MigrationStatusCompleted,
Progress: 0,
})
return ms
}
// GetProgress 获取当前进度
func (ms *MigrationService) GetProgress() MigrationProgress {
return ms.progress.Load().(MigrationProgress)
}
// updateProgress 更新进度
func (ms *MigrationService) updateProgress(progress MigrationProgress) {
ms.progress.Store(progress)
}
// MigrateDirectory 迁移目录
func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
// 创建可取消的上下文
ctx, cancel := context.WithCancel(context.Background())
ms.mu.Lock()
ms.ctx = ctx
ms.cancel = cancel
ms.mu.Unlock()
defer func() {
ms.mu.Lock()
ms.cancel = nil
ms.ctx = nil
ms.mu.Unlock()
}()
// 初始化进度
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 0,
})
// 预检查
if err := ms.preCheck(srcPath, dstPath); err != nil {
if err == errNoMigrationNeeded {
ms.updateProgress(MigrationProgress{
Status: MigrationStatusCompleted,
Progress: 100,
})
return nil
}
return ms.failWithError(err)
}
// 执行原子迁移
if err := ms.atomicMove(ctx, srcPath, dstPath); err != nil {
return ms.failWithError(err)
}
// 迁移完成
ms.updateProgress(MigrationProgress{
Status: MigrationStatusCompleted,
Progress: 100,
})
return nil
}
var errNoMigrationNeeded = fmt.Errorf("no migration needed")
// preCheck 预检查
func (ms *MigrationService) preCheck(srcPath, dstPath string) error {
// 检查源目录是否存在
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
return errNoMigrationNeeded
}
// 如果路径相同,不需要迁移
srcAbs, _ := filepath.Abs(srcPath)
dstAbs, _ := filepath.Abs(dstPath)
if srcAbs == dstAbs {
return errNoMigrationNeeded
}
// 检查目标路径是否是源路径的子目录
if ms.isSubDirectory(srcAbs, dstAbs) {
return fmt.Errorf("target path cannot be a subdirectory of source path")
}
return nil
}
// failWithError 失败并记录错误
func (ms *MigrationService) failWithError(err error) error {
ms.updateProgress(MigrationProgress{
Status: MigrationStatusFailed,
Error: err.Error(),
})
return err
}
// atomicMove 原子移动目录
func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath string) error {
// 检查是否取消
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// 确保目标目录的父目录存在
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
return fmt.Errorf("failed to create target parent directory: %v", err)
}
// 检查目标路径
if err := ms.checkTargetPath(dstPath); err != nil {
return err
}
// 尝试直接重命名
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 20,
})
if err := os.Rename(srcPath, dstPath); err == nil {
return nil
}
// 重命名失败,使用压缩迁移
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 30,
})
return ms.compressMove(ctx, srcPath, dstPath)
}
// checkTargetPath 检查目标路径
func (ms *MigrationService) checkTargetPath(dstPath string) error {
stat, err := os.Stat(dstPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("failed to check target path: %v", err)
}
if !stat.IsDir() {
return fmt.Errorf("target path exists but is not a directory")
}
isEmpty, err := ms.isDirectoryEmpty(dstPath)
if err != nil {
return fmt.Errorf("failed to check target directory: %v", err)
}
if !isEmpty {
return fmt.Errorf("target directory is not empty")
}
return nil
}
// compressMove 压缩迁移
func (ms *MigrationService) compressMove(ctx context.Context, srcPath, dstPath string) error {
tempZipFile := filepath.Join(os.TempDir(),
fmt.Sprintf("voidraft_migration_%d.zip", time.Now().UnixNano()))
defer os.Remove(tempZipFile)
// 压缩源目录
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 40,
})
if err := ms.compressDirectory(ctx, srcPath, tempZipFile); err != nil {
return fmt.Errorf("failed to compress source directory: %v", err)
}
// 解压到目标位置
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 70,
})
if err := ms.extractToDirectory(ctx, tempZipFile, dstPath); err != nil {
return fmt.Errorf("failed to extract to target location: %v", err)
}
// 检查是否取消
select {
case <-ctx.Done():
os.RemoveAll(dstPath)
return ctx.Err()
default:
}
// 删除源目录
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 90,
})
os.RemoveAll(srcPath)
return nil
}
// compressDirectory 压缩目录到zip文件
func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFile string) error {
zipWriter, err := os.Create(zipFile)
if err != nil {
return err
}
defer zipWriter.Close()
zw := zip.NewWriter(zipWriter)
defer zw.Close()
return filepath.Walk(srcDir, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 检查是否取消
select {
case <-ctx.Done():
return ctx.Err()
default:
}
relPath, err := filepath.Rel(srcDir, filePath)
if err != nil || relPath == "." {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = strings.ReplaceAll(relPath, string(filepath.Separator), "/")
if info.IsDir() {
header.Name += "/"
header.Method = zip.Store
} else {
header.Method = zip.Deflate
}
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
return ms.copyFileToZip(filePath, writer)
}
return nil
})
}
// copyFileToZip 复制文件到zip
func (ms *MigrationService) copyFileToZip(filePath string, writer io.Writer) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
return err
}
// extractToDirectory 从zip文件解压到目录
func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dstDir string) error {
reader, err := zip.OpenReader(zipFile)
if err != nil {
return err
}
defer reader.Close()
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
for _, file := range reader.File {
// 检查是否取消
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := ms.extractSingleFile(file, dstDir); err != nil {
return err
}
}
return nil
}
// extractSingleFile 解压单个文件
func (ms *MigrationService) extractSingleFile(file *zip.File, dstDir string) error {
dstPath := filepath.Join(dstDir, file.Name)
// 安全检查防止zip slip攻击
if !strings.HasPrefix(dstPath, filepath.Clean(dstDir)+string(os.PathSeparator)) {
return fmt.Errorf("invalid file path in archive: %s", file.Name)
}
if file.FileInfo().IsDir() {
return os.MkdirAll(dstPath, file.FileInfo().Mode())
}
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, file.FileInfo().Mode())
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}
// isDirectoryEmpty 检查目录是否为空
func (ms *MigrationService) isDirectoryEmpty(dirPath string) (bool, error) {
f, err := os.Open(dirPath)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdir(1)
return err == io.EOF, nil
}
// isSubDirectory 检查target是否是parent的子目录
func (ms *MigrationService) isSubDirectory(parent, target string) bool {
parent = filepath.Clean(parent) + string(filepath.Separator)
target = filepath.Clean(target) + string(filepath.Separator)
return len(target) > len(parent) && strings.HasPrefix(target, parent)
}
// CancelMigration 取消迁移
func (ms *MigrationService) CancelMigration() error {
ms.mu.Lock()
defer ms.mu.Unlock()
if ms.cancel != nil {
ms.cancel()
return nil
}
return fmt.Errorf("no active migration to cancel")
}
// ServiceShutdown 服务关闭
func (ms *MigrationService) ServiceShutdown() error {
ms.CancelMigration()
return nil
}