feat: add checksum-based update detection

This commit is contained in:
Mikhail Kiselev
2026-02-10 15:53:26 +03:00
parent 75dc03b0fd
commit 2b86eb9d20
4 changed files with 87 additions and 4 deletions

View File

@@ -2,6 +2,8 @@ package updater
import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
@@ -43,6 +45,7 @@ type UpdateInfo struct {
ReleaseNotes string `json:"releaseNotes"`
DownloadURL string `json:"downloadURL"`
DownloadSize int64 `json:"downloadSize"`
IsRebuild bool `json:"isRebuild"`
}
type Updater struct {
@@ -89,16 +92,40 @@ func (u *Updater) CheckForUpdates() (*UpdateInfo, error) {
u.downloadURL = downloadAsset.BrowserDownloadURL
// Find checksum asset
var checksumAsset *Asset
for i := range release.Assets {
if strings.Contains(release.Assets[i].Name, "macos-arm64") && strings.HasSuffix(release.Assets[i].Name, ".sha256") {
checksumAsset = &release.Assets[i]
break
}
}
latestVersion := strings.TrimPrefix(release.TagName, "v")
currentVersion := strings.TrimPrefix(version.Version, "v")
isNewer := isNewerVersion(latestVersion, currentVersion)
isRebuild := false
// Check if same version but different checksum (rebuild)
if !isNewer && checksumAsset != nil {
remoteChecksum, err := u.downloadChecksum(checksumAsset.BrowserDownloadURL)
if err == nil {
localChecksum, err := u.calculateBinaryChecksum()
if err == nil && remoteChecksum != localChecksum {
isRebuild = true
}
}
}
info := &UpdateInfo{
Available: isNewerVersion(latestVersion, currentVersion),
Available: isNewer || isRebuild,
CurrentVersion: version.Version,
LatestVersion: release.TagName,
ReleaseNotes: release.Body,
DownloadURL: downloadAsset.BrowserDownloadURL,
DownloadSize: downloadAsset.Size,
IsRebuild: isRebuild,
}
return info, nil
@@ -304,6 +331,54 @@ func (u *Updater) copyDir(src, dst string) error {
})
}
// downloadChecksum fetches the remote SHA256 checksum file
func (u *Updater) downloadChecksum(url string) (string, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return "", err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download checksum: status %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
// calculateBinaryChecksum calculates SHA256 of the current running binary
func (u *Updater) calculateBinaryChecksum() (string, error) {
execPath, err := os.Executable()
if err != nil {
return "", err
}
// Resolve symlinks to get actual binary path
execPath, err = filepath.EvalSymlinks(execPath)
if err != nil {
return "", err
}
file, err := os.Open(execPath)
if err != nil {
return "", err
}
defer func() { _ = file.Close() }()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
// isNewerVersion compares semver-like versions (e.g., "0.1.0" vs "0.2.0")
func isNewerVersion(latest, current string) bool {
if current == "dev" || current == "unknown" {