feat: add checksum-based update detection
This commit is contained in:
6
Makefile
6
Makefile
@@ -64,8 +64,10 @@ release: lint
|
|||||||
@xattr -cr "build/bin/Daily Timer.app" 2>/dev/null || true
|
@xattr -cr "build/bin/Daily Timer.app" 2>/dev/null || true
|
||||||
@rm -rf dist && mkdir -p dist
|
@rm -rf dist && mkdir -p dist
|
||||||
cd build/bin && zip -r "../../dist/Daily-Timer-$(VERSION)-macos-arm64.zip" "Daily Timer.app"
|
cd build/bin && zip -r "../../dist/Daily-Timer-$(VERSION)-macos-arm64.zip" "Daily Timer.app"
|
||||||
|
@shasum -a 256 "build/bin/Daily Timer.app/Contents/MacOS/daily-timer" | awk '{print $$1}' > "dist/Daily-Timer-$(VERSION)-macos-arm64.sha256"
|
||||||
@echo "Release package: dist/Daily-Timer-$(VERSION)-macos-arm64.zip"
|
@echo "Release package: dist/Daily-Timer-$(VERSION)-macos-arm64.zip"
|
||||||
@ls -lh dist/*.zip
|
@echo "Checksum: $$(cat dist/Daily-Timer-$(VERSION)-macos-arm64.sha256)"
|
||||||
|
@ls -lh dist/*
|
||||||
|
|
||||||
# Release for both architectures
|
# Release for both architectures
|
||||||
release-all: lint
|
release-all: lint
|
||||||
@@ -89,7 +91,7 @@ release-upload:
|
|||||||
-d '{"tag_name": "$(VERSION)", "name": "$(VERSION)", "body": "Release $(VERSION)"}' \
|
-d '{"tag_name": "$(VERSION)", "name": "$(VERSION)", "body": "Release $(VERSION)"}' \
|
||||||
| jq -r '.id'); \
|
| jq -r '.id'); \
|
||||||
echo "Created release ID: $$RELEASE_ID"; \
|
echo "Created release ID: $$RELEASE_ID"; \
|
||||||
for file in dist/*.zip; do \
|
for file in dist/*; do \
|
||||||
filename=$$(basename "$$file"); \
|
filename=$$(basename "$$file"); \
|
||||||
echo "Uploading $$filename..."; \
|
echo "Uploading $$filename..."; \
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
|
|||||||
@@ -309,7 +309,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if updateInfo?.available}
|
{:else if updateInfo?.available}
|
||||||
<div class="update-status available">
|
<div class="update-status available">
|
||||||
|
{#if updateInfo.isRebuild}
|
||||||
|
{$t('updates.rebuildAvailable')}: <strong>{updateInfo.latestVersion}</strong>
|
||||||
|
{:else}
|
||||||
{$t('updates.updateAvailable')}: <strong>{updateInfo.latestVersion}</strong>
|
{$t('updates.updateAvailable')}: <strong>{updateInfo.latestVersion}</strong>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button class="update-btn primary" on:click={downloadAndInstall}>
|
<button class="update-btn primary" on:click={downloadAndInstall}>
|
||||||
{$t('updates.downloadAndInstall')}
|
{$t('updates.downloadAndInstall')}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const translations = {
|
|||||||
currentVersion: 'Текущая версия',
|
currentVersion: 'Текущая версия',
|
||||||
checkingForUpdates: 'Проверка обновлений...',
|
checkingForUpdates: 'Проверка обновлений...',
|
||||||
updateAvailable: 'Доступно обновление',
|
updateAvailable: 'Доступно обновление',
|
||||||
|
rebuildAvailable: 'Доступна пересборка',
|
||||||
upToDate: 'У вас последняя версия',
|
upToDate: 'У вас последняя версия',
|
||||||
downloadAndInstall: 'Скачать и установить',
|
downloadAndInstall: 'Скачать и установить',
|
||||||
downloading: 'Загрузка...',
|
downloading: 'Загрузка...',
|
||||||
@@ -281,6 +282,7 @@ export const translations = {
|
|||||||
currentVersion: 'Current version',
|
currentVersion: 'Current version',
|
||||||
checkingForUpdates: 'Checking for updates...',
|
checkingForUpdates: 'Checking for updates...',
|
||||||
updateAvailable: 'Update available',
|
updateAvailable: 'Update available',
|
||||||
|
rebuildAvailable: 'Rebuild available',
|
||||||
upToDate: 'You have the latest version',
|
upToDate: 'You have the latest version',
|
||||||
downloadAndInstall: 'Download and install',
|
downloadAndInstall: 'Download and install',
|
||||||
downloading: 'Downloading...',
|
downloading: 'Downloading...',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package updater
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -43,6 +45,7 @@ type UpdateInfo struct {
|
|||||||
ReleaseNotes string `json:"releaseNotes"`
|
ReleaseNotes string `json:"releaseNotes"`
|
||||||
DownloadURL string `json:"downloadURL"`
|
DownloadURL string `json:"downloadURL"`
|
||||||
DownloadSize int64 `json:"downloadSize"`
|
DownloadSize int64 `json:"downloadSize"`
|
||||||
|
IsRebuild bool `json:"isRebuild"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
@@ -89,16 +92,40 @@ func (u *Updater) CheckForUpdates() (*UpdateInfo, error) {
|
|||||||
|
|
||||||
u.downloadURL = downloadAsset.BrowserDownloadURL
|
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")
|
latestVersion := strings.TrimPrefix(release.TagName, "v")
|
||||||
currentVersion := strings.TrimPrefix(version.Version, "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{
|
info := &UpdateInfo{
|
||||||
Available: isNewerVersion(latestVersion, currentVersion),
|
Available: isNewer || isRebuild,
|
||||||
CurrentVersion: version.Version,
|
CurrentVersion: version.Version,
|
||||||
LatestVersion: release.TagName,
|
LatestVersion: release.TagName,
|
||||||
ReleaseNotes: release.Body,
|
ReleaseNotes: release.Body,
|
||||||
DownloadURL: downloadAsset.BrowserDownloadURL,
|
DownloadURL: downloadAsset.BrowserDownloadURL,
|
||||||
DownloadSize: downloadAsset.Size,
|
DownloadSize: downloadAsset.Size,
|
||||||
|
IsRebuild: isRebuild,
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, nil
|
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")
|
// isNewerVersion compares semver-like versions (e.g., "0.1.0" vs "0.2.0")
|
||||||
func isNewerVersion(latest, current string) bool {
|
func isNewerVersion(latest, current string) bool {
|
||||||
if current == "dev" || current == "unknown" {
|
if current == "dev" || current == "unknown" {
|
||||||
|
|||||||
Reference in New Issue
Block a user