- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等 - 实现了与 MirrorChyan API 交互的客户端逻辑 - 添加了版本检查、更新检测和下载 URL 生成等功能 - 嵌入了配置模板和资源文件系统 - 提供了完整的构建和发布流程
475 lines
14 KiB
Go
475 lines
14 KiB
Go
package install
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// ChangesInfo represents the structure of changes.json file
|
|
type ChangesInfo struct {
|
|
Deleted []string `json:"deleted"`
|
|
Added []string `json:"added"`
|
|
Modified []string `json:"modified"`
|
|
}
|
|
|
|
// InstallManager interface defines the contract for installation operations
|
|
type InstallManager interface {
|
|
ExtractZip(zipPath, destPath string) error
|
|
ProcessChanges(changesPath string) (*ChangesInfo, error)
|
|
ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error
|
|
HandleRunningProcess(processName string) error
|
|
CreateTempDir() (string, error)
|
|
CleanupTempDir(tempDir string) error
|
|
}
|
|
|
|
// Manager implements the InstallManager interface
|
|
type Manager struct {
|
|
tempDirs []string // Track temporary directories for cleanup
|
|
}
|
|
|
|
// NewManager creates a new install manager instance
|
|
func NewManager() *Manager {
|
|
return &Manager{
|
|
tempDirs: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
// CreateTempDir creates a temporary directory for extraction
|
|
func (m *Manager) CreateTempDir() (string, error) {
|
|
tempDir, err := os.MkdirTemp("", "updater_*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp directory: %w", err)
|
|
}
|
|
|
|
// Track temp directory for cleanup
|
|
m.tempDirs = append(m.tempDirs, tempDir)
|
|
return tempDir, nil
|
|
}
|
|
|
|
// CleanupTempDir removes a temporary directory and its contents
|
|
func (m *Manager) CleanupTempDir(tempDir string) error {
|
|
if tempDir == "" {
|
|
return nil
|
|
}
|
|
|
|
err := os.RemoveAll(tempDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to cleanup temp directory %s: %w", tempDir, err)
|
|
}
|
|
|
|
// Remove from tracking list
|
|
for i, dir := range m.tempDirs {
|
|
if dir == tempDir {
|
|
m.tempDirs = append(m.tempDirs[:i], m.tempDirs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupAllTempDirs removes all tracked temporary directories
|
|
func (m *Manager) CleanupAllTempDirs() error {
|
|
var errors []string
|
|
|
|
for _, tempDir := range m.tempDirs {
|
|
if err := os.RemoveAll(tempDir); err != nil {
|
|
errors = append(errors, fmt.Sprintf("failed to cleanup %s: %v", tempDir, err))
|
|
}
|
|
}
|
|
|
|
m.tempDirs = m.tempDirs[:0] // Clear the slice
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("cleanup errors: %s", strings.Join(errors, "; "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExtractZip extracts a ZIP file to the specified destination directory
|
|
func (m *Manager) ExtractZip(zipPath, destPath string) error {
|
|
// Open ZIP file for reading
|
|
reader, err := zip.OpenReader(zipPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open ZIP file %s: %w", zipPath, err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Create destination directory if it doesn't exist
|
|
if err := os.MkdirAll(destPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create destination directory %s: %w", destPath, err)
|
|
}
|
|
|
|
// Extract files
|
|
for _, file := range reader.File {
|
|
if err := m.extractFile(file, destPath); err != nil {
|
|
return fmt.Errorf("failed to extract file %s: %w", file.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractFile extracts a single file from the ZIP archive
|
|
func (m *Manager) extractFile(file *zip.File, destPath string) error {
|
|
// Clean the file path to prevent directory traversal attacks
|
|
cleanPath := filepath.Clean(file.Name)
|
|
if strings.Contains(cleanPath, "..") {
|
|
return fmt.Errorf("invalid file path: %s", file.Name)
|
|
}
|
|
|
|
// Create full destination path
|
|
destFile := filepath.Join(destPath, cleanPath)
|
|
|
|
// Create directory structure if needed
|
|
if file.FileInfo().IsDir() {
|
|
return os.MkdirAll(destFile, file.FileInfo().Mode())
|
|
}
|
|
|
|
// Create parent directories
|
|
if err := os.MkdirAll(filepath.Dir(destFile), 0755); err != nil {
|
|
return fmt.Errorf("failed to create parent directory: %w", err)
|
|
}
|
|
|
|
// Open file in ZIP archive
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file in archive: %w", err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
// Create destination file
|
|
outFile, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create destination file: %w", err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Copy file contents
|
|
_, err = io.Copy(outFile, rc)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy file contents: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProcessChanges reads and parses the changes.json file
|
|
func (m *Manager) ProcessChanges(changesPath string) (*ChangesInfo, error) {
|
|
// Check if changes.json exists
|
|
if _, err := os.Stat(changesPath); os.IsNotExist(err) {
|
|
// If changes.json doesn't exist, return empty changes info
|
|
return &ChangesInfo{
|
|
Deleted: []string{},
|
|
Added: []string{},
|
|
Modified: []string{},
|
|
}, nil
|
|
}
|
|
|
|
// Read the changes.json file
|
|
data, err := os.ReadFile(changesPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read changes file %s: %w", changesPath, err)
|
|
}
|
|
|
|
// Parse JSON
|
|
var changes ChangesInfo
|
|
if err := json.Unmarshal(data, &changes); err != nil {
|
|
return nil, fmt.Errorf("failed to parse changes JSON: %w", err)
|
|
}
|
|
|
|
return &changes, nil
|
|
}
|
|
|
|
// HandleRunningProcess handles running processes by renaming files that are in use
|
|
func (m *Manager) HandleRunningProcess(processName string) error {
|
|
// Get the current executable path
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get executable path: %w", err)
|
|
}
|
|
|
|
exeDir := filepath.Dir(exePath)
|
|
targetFile := filepath.Join(exeDir, processName)
|
|
|
|
// Check if the target file exists
|
|
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
|
// File doesn't exist, nothing to handle
|
|
return nil
|
|
}
|
|
|
|
// Try to rename the file to indicate it should be deleted on next startup
|
|
oldFile := targetFile + ".old"
|
|
|
|
// Remove existing .old file if it exists
|
|
if _, err := os.Stat(oldFile); err == nil {
|
|
if err := os.Remove(oldFile); err != nil {
|
|
return fmt.Errorf("failed to remove existing old file %s: %w", oldFile, err)
|
|
}
|
|
}
|
|
|
|
// Rename the current file to .old
|
|
if err := os.Rename(targetFile, oldFile); err != nil {
|
|
// If rename fails, the process might be running
|
|
// On Windows, we can't rename a running executable
|
|
if isFileInUse(err) {
|
|
// Mark the file for deletion on next reboot (Windows specific)
|
|
return m.markFileForDeletion(targetFile)
|
|
}
|
|
return fmt.Errorf("failed to rename running process file %s: %w", targetFile, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isFileInUse checks if the error indicates the file is in use
|
|
func isFileInUse(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for Windows-specific "file in use" errors
|
|
if pathErr, ok := err.(*os.PathError); ok {
|
|
if errno, ok := pathErr.Err.(syscall.Errno); ok {
|
|
// ERROR_SHARING_VIOLATION (32) or ERROR_ACCESS_DENIED (5)
|
|
return errno == syscall.Errno(32) || errno == syscall.Errno(5)
|
|
}
|
|
}
|
|
|
|
return strings.Contains(err.Error(), "being used by another process") ||
|
|
strings.Contains(err.Error(), "access is denied")
|
|
}
|
|
|
|
// markFileForDeletion marks a file for deletion on next system reboot (Windows specific)
|
|
func (m *Manager) markFileForDeletion(filePath string) error {
|
|
// This is a Windows-specific implementation
|
|
// For now, we'll create a marker file that can be handled by the main application
|
|
markerFile := filePath + ".delete_on_restart"
|
|
|
|
// Create a marker file
|
|
file, err := os.Create(markerFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create deletion marker file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Write the target file path to the marker
|
|
_, err = file.WriteString(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write to marker file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteMarkedFiles removes files that were marked for deletion
|
|
func (m *Manager) DeleteMarkedFiles(directory string) error {
|
|
// Find all .delete_on_restart files
|
|
pattern := filepath.Join(directory, "*.delete_on_restart")
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find marker files: %w", err)
|
|
}
|
|
|
|
var errors []string
|
|
for _, markerFile := range matches {
|
|
// Read the target file path
|
|
data, err := os.ReadFile(markerFile)
|
|
if err != nil {
|
|
errors = append(errors, fmt.Sprintf("failed to read marker file %s: %v", markerFile, err))
|
|
continue
|
|
}
|
|
|
|
targetFile := strings.TrimSpace(string(data))
|
|
|
|
// Try to delete the target file
|
|
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
|
|
errors = append(errors, fmt.Sprintf("failed to delete marked file %s: %v", targetFile, err))
|
|
}
|
|
|
|
// Remove the marker file
|
|
if err := os.Remove(markerFile); err != nil {
|
|
errors = append(errors, fmt.Sprintf("failed to remove marker file %s: %v", markerFile, err))
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("deletion errors: %s", strings.Join(errors, "; "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApplyUpdate applies the update by copying files from source to target directory
|
|
func (m *Manager) ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error {
|
|
// Create backup directory
|
|
backupDir, err := m.createBackupDir(targetPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Backup existing files before applying update
|
|
if err := m.backupFiles(targetPath, backupDir, changes); err != nil {
|
|
return fmt.Errorf("failed to backup files: %w", err)
|
|
}
|
|
|
|
// Apply the update
|
|
if err := m.applyUpdateFiles(sourcePath, targetPath, changes); err != nil {
|
|
// Rollback on failure
|
|
if rollbackErr := m.rollbackUpdate(targetPath, backupDir); rollbackErr != nil {
|
|
return fmt.Errorf("update failed and rollback failed: update error: %w, rollback error: %v", err, rollbackErr)
|
|
}
|
|
return fmt.Errorf("update failed and was rolled back: %w", err)
|
|
}
|
|
|
|
// Clean up backup directory after successful update
|
|
if err := os.RemoveAll(backupDir); err != nil {
|
|
// Log warning but don't fail the update
|
|
fmt.Printf("Warning: failed to cleanup backup directory %s: %v\n", backupDir, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createBackupDir creates a backup directory for the update
|
|
func (m *Manager) createBackupDir(targetPath string) (string, error) {
|
|
backupDir := filepath.Join(targetPath, ".backup_"+fmt.Sprintf("%d", os.Getpid()))
|
|
|
|
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
|
return "", fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
return backupDir, nil
|
|
}
|
|
|
|
// backupFiles creates backups of files that will be modified or deleted
|
|
func (m *Manager) backupFiles(targetPath, backupDir string, changes *ChangesInfo) error {
|
|
// Backup files that will be modified
|
|
for _, file := range changes.Modified {
|
|
srcFile := filepath.Join(targetPath, file)
|
|
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
|
|
continue // File doesn't exist, skip backup
|
|
}
|
|
|
|
backupFile := filepath.Join(backupDir, file)
|
|
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
|
|
return fmt.Errorf("failed to backup modified file %s: %w", file, err)
|
|
}
|
|
}
|
|
|
|
// Backup files that will be deleted
|
|
for _, file := range changes.Deleted {
|
|
srcFile := filepath.Join(targetPath, file)
|
|
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
|
|
continue // File doesn't exist, skip backup
|
|
}
|
|
|
|
backupFile := filepath.Join(backupDir, file)
|
|
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
|
|
return fmt.Errorf("failed to backup deleted file %s: %w", file, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyUpdateFiles applies the actual file changes
|
|
func (m *Manager) applyUpdateFiles(sourcePath, targetPath string, changes *ChangesInfo) error {
|
|
// Delete files marked for deletion
|
|
for _, file := range changes.Deleted {
|
|
targetFile := filepath.Join(targetPath, file)
|
|
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to delete file %s: %w", file, err)
|
|
}
|
|
}
|
|
|
|
// Copy new and modified files
|
|
filesToCopy := append(changes.Added, changes.Modified...)
|
|
for _, file := range filesToCopy {
|
|
srcFile := filepath.Join(sourcePath, file)
|
|
targetFile := filepath.Join(targetPath, file)
|
|
|
|
// Check if source file exists
|
|
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
|
|
continue // Source file doesn't exist, skip
|
|
}
|
|
|
|
if err := m.copyFileWithDirs(srcFile, targetFile); err != nil {
|
|
return fmt.Errorf("failed to copy file %s: %w", file, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// copyFileWithDirs copies a file and creates necessary directories
|
|
func (m *Manager) copyFileWithDirs(src, dst string) error {
|
|
// Create parent directories
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
|
return fmt.Errorf("failed to create parent directories: %w", err)
|
|
}
|
|
|
|
// Open source file
|
|
srcFile, err := os.Open(src)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open source file: %w", err)
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
// Get source file info
|
|
srcInfo, err := srcFile.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get source file info: %w", err)
|
|
}
|
|
|
|
// Create destination file
|
|
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create destination file: %w", err)
|
|
}
|
|
defer dstFile.Close()
|
|
|
|
// Copy file contents
|
|
_, err = io.Copy(dstFile, srcFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy file contents: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rollbackUpdate restores files from backup in case of update failure
|
|
func (m *Manager) rollbackUpdate(targetPath, backupDir string) error {
|
|
// Walk through backup directory and restore files
|
|
return filepath.Walk(backupDir, func(backupFile string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil // Skip directories
|
|
}
|
|
|
|
// Calculate relative path
|
|
relPath, err := filepath.Rel(backupDir, backupFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to calculate relative path: %w", err)
|
|
}
|
|
|
|
// Restore file to target location
|
|
targetFile := filepath.Join(targetPath, relPath)
|
|
if err := m.copyFileWithDirs(backupFile, targetFile); err != nil {
|
|
return fmt.Errorf("failed to restore file %s: %w", relPath, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|