feat(Go_Updater): 添加全新 Go 语言实现的自动更新器
- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等 - 实现了与 MirrorChyan API 交互的客户端逻辑 - 添加了版本检查、更新检测和下载 URL 生成等功能 - 嵌入了配置模板和资源文件系统 - 提供了完整的构建和发布流程
This commit is contained in:
224
Go_Updater/download/manager.go
Normal file
224
Go_Updater/download/manager.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DownloadProgress represents the current download progress
|
||||
type DownloadProgress struct {
|
||||
BytesDownloaded int64
|
||||
TotalBytes int64
|
||||
Percentage float64
|
||||
Speed int64 // bytes per second
|
||||
}
|
||||
|
||||
// ProgressCallback is called during download to report progress
|
||||
type ProgressCallback func(DownloadProgress)
|
||||
|
||||
// DownloadManager interface defines download operations
|
||||
type DownloadManager interface {
|
||||
Download(url, destination string, progressCallback ProgressCallback) error
|
||||
DownloadWithResume(url, destination string, progressCallback ProgressCallback) error
|
||||
ValidateChecksum(filePath, expectedChecksum string) error
|
||||
SetTimeout(timeout time.Duration)
|
||||
}
|
||||
|
||||
// Manager implements DownloadManager interface
|
||||
type Manager struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewManager creates a new download manager
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Download downloads a file from the given URL to the destination path
|
||||
func (m *Manager) Download(url, destination string, progressCallback ProgressCallback) error {
|
||||
return m.downloadWithContext(context.Background(), url, destination, progressCallback, false)
|
||||
}
|
||||
|
||||
// DownloadWithResume downloads a file with resume capability
|
||||
func (m *Manager) DownloadWithResume(url, destination string, progressCallback ProgressCallback) error {
|
||||
return m.downloadWithContext(context.Background(), url, destination, progressCallback, true)
|
||||
}
|
||||
|
||||
// downloadWithContext performs the actual download with context support
|
||||
func (m *Manager) downloadWithContext(ctx context.Context, url, destination string, progressCallback ProgressCallback, resume bool) error {
|
||||
// Create destination directory if it doesn't exist
|
||||
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if file exists for resume
|
||||
var existingSize int64
|
||||
if resume {
|
||||
if stat, err := os.Stat(destination); err == nil {
|
||||
existingSize = stat.Size()
|
||||
}
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Add range header for resume
|
||||
if resume && existingSize > 0 {
|
||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", existingSize))
|
||||
}
|
||||
|
||||
// Execute request
|
||||
resp, err := m.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Get total size
|
||||
totalSize := existingSize
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil {
|
||||
totalSize += size
|
||||
}
|
||||
}
|
||||
|
||||
// Open destination file
|
||||
var file *os.File
|
||||
if resume && existingSize > 0 {
|
||||
file, err = os.OpenFile(destination, os.O_WRONLY|os.O_APPEND, 0644)
|
||||
} else {
|
||||
file, err = os.Create(destination)
|
||||
existingSize = 0
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Download with progress tracking
|
||||
return m.copyWithProgress(resp.Body, file, existingSize, totalSize, progressCallback)
|
||||
}
|
||||
|
||||
// copyWithProgress copies data while tracking progress
|
||||
func (m *Manager) copyWithProgress(src io.Reader, dst io.Writer, startBytes, totalBytes int64, progressCallback ProgressCallback) error {
|
||||
buffer := make([]byte, 32*1024) // 32KB buffer
|
||||
downloaded := startBytes
|
||||
startTime := time.Now()
|
||||
lastUpdate := startTime
|
||||
|
||||
for {
|
||||
n, err := src.Read(buffer)
|
||||
if n > 0 {
|
||||
if _, writeErr := dst.Write(buffer[:n]); writeErr != nil {
|
||||
return fmt.Errorf("failed to write to destination: %w", writeErr)
|
||||
}
|
||||
downloaded += int64(n)
|
||||
|
||||
// Update progress every 100ms
|
||||
now := time.Now()
|
||||
if progressCallback != nil && now.Sub(lastUpdate) >= 100*time.Millisecond {
|
||||
elapsed := now.Sub(startTime).Seconds()
|
||||
speed := int64(0)
|
||||
if elapsed > 0 {
|
||||
speed = int64(float64(downloaded-startBytes) / elapsed)
|
||||
}
|
||||
|
||||
percentage := float64(0)
|
||||
if totalBytes > 0 {
|
||||
percentage = float64(downloaded) / float64(totalBytes) * 100
|
||||
}
|
||||
|
||||
progressCallback(DownloadProgress{
|
||||
BytesDownloaded: downloaded,
|
||||
TotalBytes: totalBytes,
|
||||
Percentage: percentage,
|
||||
Speed: speed,
|
||||
})
|
||||
lastUpdate = now
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read from source: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Final progress update
|
||||
if progressCallback != nil {
|
||||
elapsed := time.Since(startTime).Seconds()
|
||||
speed := int64(0)
|
||||
if elapsed > 0 {
|
||||
speed = int64(float64(downloaded-startBytes) / elapsed)
|
||||
}
|
||||
|
||||
percentage := float64(100)
|
||||
if totalBytes > 0 {
|
||||
percentage = float64(downloaded) / float64(totalBytes) * 100
|
||||
}
|
||||
|
||||
progressCallback(DownloadProgress{
|
||||
BytesDownloaded: downloaded,
|
||||
TotalBytes: totalBytes,
|
||||
Percentage: percentage,
|
||||
Speed: speed,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateChecksum validates the SHA256 checksum of a file
|
||||
func (m *Manager) ValidateChecksum(filePath, expectedChecksum string) error {
|
||||
if expectedChecksum == "" {
|
||||
return nil // No checksum to validate
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file for checksum validation: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
|
||||
actualChecksum := hex.EncodeToString(hash.Sum(nil))
|
||||
if actualChecksum != expectedChecksum {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimeout sets the timeout for download operations
|
||||
func (m *Manager) SetTimeout(timeout time.Duration) {
|
||||
m.timeout = timeout
|
||||
m.client.Timeout = timeout
|
||||
}
|
||||
Reference in New Issue
Block a user