145 lines
3.7 KiB
Go
145 lines
3.7 KiB
Go
package update
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
)
|
|
|
|
// VerifyChecksum verifies the SHA256 checksum of the downloaded file
|
|
func VerifyChecksum(ctx context.Context, filePath, checksumURL, expectedFilename string) error {
|
|
manager.setProgress(PhaseVerifying, "正在校验文件完整性...", 50)
|
|
|
|
// Compute SHA256 of the downloaded file
|
|
actualHash, err := computeSHA256(ctx, filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to compute SHA256: %w", err)
|
|
}
|
|
|
|
g.Log().Debugf(ctx, "Computed SHA256: %s", actualHash)
|
|
|
|
// 缺少校验文件地址时禁止放行:无法确认更新包完整性,宁可中止升级
|
|
if checksumURL == "" {
|
|
return fmt.Errorf("release 未提供 checksums-sha256.txt,无法校验更新包完整性,已中止更新")
|
|
}
|
|
|
|
// Download checksums file
|
|
expectedHash, err := fetchExpectedHash(ctx, checksumURL, expectedFilename)
|
|
if err != nil {
|
|
// 拉不到校验文件同样不放行,防止跳过校验把破损/篡改的包替换上线
|
|
return fmt.Errorf("获取校验文件失败: %w", err)
|
|
}
|
|
|
|
manager.setProgress(PhaseVerifying, "正在对比校验值...", 80)
|
|
|
|
if !strings.EqualFold(actualHash, expectedHash) {
|
|
return fmt.Errorf("SHA256 校验不一致(期望 %s,实际 %s),下载的文件可能已破损或被篡改,已中止更新", expectedHash, actualHash)
|
|
}
|
|
|
|
manager.setProgress(PhaseVerifying, "文件校验通过", 100)
|
|
g.Log().Info(ctx, "File checksum verified successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
// computeSHA256 computes the SHA256 hash of a file with progress reporting
|
|
func computeSHA256(ctx context.Context, filePath string) (string, error) {
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hash := sha256.New()
|
|
buf := make([]byte, 64*1024) // 64KB buffer
|
|
var processed int64
|
|
var lastReport time.Time
|
|
|
|
for {
|
|
nr, err := f.Read(buf)
|
|
if nr > 0 {
|
|
hash.Write(buf[:nr])
|
|
processed += int64(nr)
|
|
}
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Update progress
|
|
now := time.Now()
|
|
if now.Sub(lastReport) > 500*time.Millisecond {
|
|
lastReport = now
|
|
percentage := 30
|
|
if info.Size() > 0 {
|
|
percentage = int(processed * 30 / info.Size())
|
|
if percentage > 29 {
|
|
percentage = 29
|
|
}
|
|
}
|
|
manager.setProgress(PhaseVerifying, "正在计算文件校验值...", percentage)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// fetchExpectedHash downloads the checksums file and finds the hash for the target filename
|
|
func fetchExpectedHash(ctx context.Context, checksumURL, filename string) (string, error) {
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
req, err := http.NewRequestWithContext(ctx, "GET", checksumURL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("checksum download returned %d", resp.StatusCode)
|
|
}
|
|
|
|
// Parse checksums file format:
|
|
// <hash> <filename>
|
|
// or
|
|
// <hash> *<filename>
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
baseFilename := filepath.Base(filename)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
hash := parts[0]
|
|
file := strings.TrimPrefix(parts[1], "*")
|
|
// 兼容历史资产命名(checksums 文件中的文件名可能带/不带 v 前缀)
|
|
if normalizeAssetName(file) == normalizeAssetName(baseFilename) {
|
|
return strings.ToLower(hash), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("checksum for %s not found in checksums file", filename)
|
|
}
|