Files
AUTO-MAS-test/Go_Updater/download/manager.go
AoXuan 228e66315c feat(Go_Updater): 添加全新 Go 语言实现的自动更新器
- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等
- 实现了与 MirrorChyan API 交互的客户端逻辑
- 添加了版本检查、更新检测和下载 URL 生成等功能
- 嵌入了配置模板和资源文件系统
- 提供了完整的构建和发布流程
2025-07-20 16:30:14 +08:00

224 lines
6.0 KiB
Go

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
}