Improve update safety and progress output
All checks were successful
release / create-release (push) Successful in 7s
release / build-and-upload (pia-qbt-autoport-linux-amd64, amd64, linux, pia-qbt-autoport-linux-amd64) (push) Successful in 53s
release / build-and-upload (pia-qbt-autoport-macos-amd64, amd64, darwin, pia-qbt-autoport-macos-amd64) (push) Successful in 58s
release / build-and-upload (pia-qbt-autoport-macos-arm64, arm64, darwin, pia-qbt-autoport-macos-arm64) (push) Successful in 52s
release / build-and-upload (pia-qbt-autoport-windows-amd64.exe, amd64, windows, pia-qbt-autoport-windows-amd64.exe) (push) Successful in 55s
All checks were successful
release / create-release (push) Successful in 7s
release / build-and-upload (pia-qbt-autoport-linux-amd64, amd64, linux, pia-qbt-autoport-linux-amd64) (push) Successful in 53s
release / build-and-upload (pia-qbt-autoport-macos-amd64, amd64, darwin, pia-qbt-autoport-macos-amd64) (push) Successful in 58s
release / build-and-upload (pia-qbt-autoport-macos-arm64, arm64, darwin, pia-qbt-autoport-macos-arm64) (push) Successful in 52s
release / build-and-upload (pia-qbt-autoport-windows-amd64.exe, amd64, windows, pia-qbt-autoport-windows-amd64.exe) (push) Successful in 55s
This commit is contained in:
@@ -96,6 +96,12 @@ jobs:
|
||||
ldflags="-s -w -X main.buildVersion=${BUILD_VERSION} -X main.buildCommit=${BUILD_COMMIT} -X main.buildDate=${BUILD_DATE}"
|
||||
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 go build -trimpath -ldflags="${ldflags}" -o dist/${{ matrix.output_name }} ./src
|
||||
|
||||
- name: Create checksum
|
||||
run: |
|
||||
set -eu
|
||||
hash=$(sha256sum "dist/${{ matrix.output_name }}" | awk '{print $1}')
|
||||
printf '%s %s\n' "$hash" "${{ matrix.asset_name }}" > "dist/${{ matrix.output_name }}.sha256"
|
||||
|
||||
- name: Upload binary
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
@@ -107,3 +113,15 @@ jobs:
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@dist/${{ matrix.output_name }}" \
|
||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${{ matrix.asset_name }}"
|
||||
|
||||
- name: Upload checksum
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||
GITEA_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
set -eu
|
||||
curl -fsS \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@dist/${{ matrix.output_name }}.sha256" \
|
||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${{ matrix.asset_name }}.sha256"
|
||||
|
||||
@@ -19,10 +19,15 @@ Update behavior is controlled by:
|
||||
|
||||
- `check_for_updates`
|
||||
- `auto_update`
|
||||
- `update_check_interval_hours`
|
||||
|
||||
Both default to `true`. Turn them off if you want to disable daily update checks or automatic self-updates.
|
||||
|
||||
When enabled, the app checks once at startup and then every 24 hours.
|
||||
`update_check_interval_hours` defaults to `24`.
|
||||
|
||||
When enabled, the app checks once at startup and then on the configured interval.
|
||||
|
||||
Each release uploads a `.sha256` checksum asset, and the updater verifies it before replacing the binary.
|
||||
|
||||
qBittorrent support is configured with:
|
||||
|
||||
|
||||
25
src/app.go
25
src/app.go
@@ -9,7 +9,6 @@ import (
|
||||
)
|
||||
|
||||
const syncInterval = 30 * time.Second
|
||||
const updateCheckInterval = 24 * time.Hour
|
||||
|
||||
type App struct {
|
||||
cfg Config
|
||||
@@ -112,7 +111,7 @@ func (a *App) qBittorrentClient() (*qBittorrentClient, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (a *App) StartUpdateChecks(ctx context.Context, exePath string, stop func()) bool {
|
||||
func (a *App) StartUpdateChecks(ctx context.Context, exePath string, stop func(), initialFailures int) bool {
|
||||
if !a.cfg.CheckForUpdates && !a.cfg.AutoUpdate {
|
||||
return false
|
||||
}
|
||||
@@ -121,15 +120,29 @@ func (a *App) StartUpdateChecks(ctx context.Context, exePath string, stop func()
|
||||
go func() {
|
||||
defer a.updateWG.Done()
|
||||
|
||||
ticker := time.NewTicker(updateCheckInterval)
|
||||
defer ticker.Stop()
|
||||
interval := a.cfg.UpdateCheckInterval()
|
||||
failures := initialFailures
|
||||
timer := time.NewTimer(updateDelay(interval, failures))
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
a.checkForUpdates(exePath, stop)
|
||||
case <-timer.C:
|
||||
switch a.checkForUpdates(exePath, stop) {
|
||||
case updateCheckFailed:
|
||||
failures++
|
||||
default:
|
||||
failures = 0
|
||||
}
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
timer.Reset(updateDelay(interval, failures))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -20,6 +21,7 @@ type Config struct {
|
||||
RunAtStartup bool `json:"run_at_startup"`
|
||||
CheckForUpdates bool `json:"check_for_updates"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
UpdateCheckIntervalHours int `json:"update_check_interval_hours"`
|
||||
QBittorrentHost string `json:"qbittorrent_host"`
|
||||
QBittorrentUsername string `json:"qbittorrent_username"`
|
||||
QBittorrentPassword string `json:"qbittorrent_password"`
|
||||
@@ -76,6 +78,9 @@ func (c *Config) normalize() error {
|
||||
if c.PIACTLPath == "" {
|
||||
c.PIACTLPath = `C:\Program Files\Private Internet Access\piactl.exe`
|
||||
}
|
||||
if c.UpdateCheckIntervalHours <= 0 {
|
||||
c.UpdateCheckIntervalHours = 24
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -121,6 +126,7 @@ func createDefaultConfig(path string) (Config, bool, error) {
|
||||
RunAtStartup: true,
|
||||
CheckForUpdates: true,
|
||||
AutoUpdate: true,
|
||||
UpdateCheckIntervalHours: 24,
|
||||
AutoSetPort: false,
|
||||
AutoSetHost: false,
|
||||
}
|
||||
@@ -176,3 +182,11 @@ func normalizeListenIP(ip string) string {
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (c Config) UpdateCheckInterval() time.Duration {
|
||||
hours := c.UpdateCheckIntervalHours
|
||||
if hours <= 0 {
|
||||
hours = 24
|
||||
}
|
||||
return time.Duration(hours) * time.Hour
|
||||
}
|
||||
|
||||
10
src/main.go
10
src/main.go
@@ -58,14 +58,20 @@ func main() {
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
app := NewApp(cfg, build)
|
||||
if app.StartUpdateChecks(ctx, exePath, stop) {
|
||||
if app.checkForUpdates(exePath, stop) {
|
||||
startupFailureCount := 0
|
||||
if cfg.CheckForUpdates || cfg.AutoUpdate {
|
||||
switch app.checkForUpdates(exePath, stop) {
|
||||
case updateCheckUpdated:
|
||||
return
|
||||
case updateCheckFailed:
|
||||
startupFailureCount = 1
|
||||
}
|
||||
} else {
|
||||
log.Printf("build version: %s", build.Version)
|
||||
log.Printf("update checks disabled")
|
||||
}
|
||||
|
||||
app.StartUpdateChecks(ctx, exePath, stop, startupFailureCount)
|
||||
app.StartBackgroundSync(ctx)
|
||||
logListenTargets(cfg)
|
||||
|
||||
|
||||
250
src/update.go
250
src/update.go
@@ -1,18 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var updateRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
type updateCheckResult int
|
||||
|
||||
const (
|
||||
updateCheckSkipped updateCheckResult = iota
|
||||
updateCheckFailed
|
||||
updateCheckNoUpdate
|
||||
updateCheckUpdated
|
||||
)
|
||||
|
||||
func maybeAutoUpdate(exePath string, release ReleaseInfo) (bool, error) {
|
||||
if buildVersion == "" || buildVersion == "dev" {
|
||||
return false, nil
|
||||
@@ -25,6 +40,10 @@ func maybeAutoUpdate(exePath string, release ReleaseInfo) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expectedHash, err := release.checksumInfo()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp(filepath.Dir(exePath), ".pia-qbt-autoport-update-*")
|
||||
if err != nil {
|
||||
@@ -38,6 +57,11 @@ func maybeAutoUpdate(exePath string, release ReleaseInfo) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := verifyFileSHA256(tempPath, expectedHash); err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := os.Chmod(tempPath, 0755); err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
@@ -54,14 +78,14 @@ func maybeAutoUpdate(exePath string, release ReleaseInfo) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *App) checkForUpdates(exePath string, stop func()) bool {
|
||||
func (a *App) checkForUpdates(exePath string, stop func()) updateCheckResult {
|
||||
if !a.cfg.CheckForUpdates && !a.cfg.AutoUpdate {
|
||||
return false
|
||||
return updateCheckSkipped
|
||||
}
|
||||
if buildVersion == "" || buildVersion == "dev" {
|
||||
log.Printf("build version: %s", buildVersion)
|
||||
log.Printf("dev build: update check skipped")
|
||||
return false
|
||||
return updateCheckSkipped
|
||||
}
|
||||
|
||||
release, err := fetchLatestReleaseInfo()
|
||||
@@ -71,32 +95,66 @@ func (a *App) checkForUpdates(exePath string, stop func()) bool {
|
||||
log.Printf("build version: %s", build.Version)
|
||||
if build.UpdateError != "" {
|
||||
log.Printf("update check unavailable: %s", build.UpdateError)
|
||||
return false
|
||||
return updateCheckFailed
|
||||
}
|
||||
|
||||
if build.UpdateAvailable {
|
||||
log.Printf("out of date: latest version is %s", build.LatestVersion)
|
||||
if !a.cfg.AutoUpdate {
|
||||
return false
|
||||
return updateCheckNoUpdate
|
||||
}
|
||||
|
||||
updated, err := maybeAutoUpdate(exePath, release)
|
||||
if err != nil {
|
||||
log.Printf("auto update failed: %v", err)
|
||||
return false
|
||||
return updateCheckFailed
|
||||
}
|
||||
if updated && stop != nil {
|
||||
stop()
|
||||
return true
|
||||
return updateCheckUpdated
|
||||
}
|
||||
return false
|
||||
return updateCheckNoUpdate
|
||||
}
|
||||
|
||||
if a.cfg.CheckForUpdates {
|
||||
log.Printf("up to date: %s", build.Version)
|
||||
}
|
||||
|
||||
return false
|
||||
return updateCheckNoUpdate
|
||||
}
|
||||
|
||||
func updateDelay(interval time.Duration, failures int) time.Duration {
|
||||
if interval <= 0 {
|
||||
interval = 24 * time.Hour
|
||||
}
|
||||
if failures <= 0 {
|
||||
return interval
|
||||
}
|
||||
|
||||
delay := 15 * time.Minute
|
||||
for i := 1; i < failures; i++ {
|
||||
delay *= 2
|
||||
if delay >= interval {
|
||||
return interval
|
||||
}
|
||||
}
|
||||
|
||||
if delay > interval {
|
||||
delay = interval
|
||||
}
|
||||
|
||||
jitter := delay / 10
|
||||
if jitter <= 0 {
|
||||
return delay
|
||||
}
|
||||
|
||||
spread := int64(jitter*2 + 1)
|
||||
offset := time.Duration(updateRand.Int63n(spread)) - jitter
|
||||
result := delay + offset
|
||||
if result < time.Minute {
|
||||
return time.Minute
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func downloadFile(rawURL, path string) error {
|
||||
@@ -122,11 +180,140 @@ func downloadFile(rawURL, path string) error {
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||
total := resp.ContentLength
|
||||
start := time.Now()
|
||||
buf := make([]byte, 32*1024)
|
||||
var downloaded int64
|
||||
lastPaint := time.Time{}
|
||||
paint := func(final bool) {
|
||||
now := time.Now()
|
||||
if !final && !lastPaint.IsZero() && now.Sub(lastPaint) < 200*time.Millisecond {
|
||||
return
|
||||
}
|
||||
lastPaint = now
|
||||
|
||||
percent := 0
|
||||
if total > 0 {
|
||||
percent = int(float64(downloaded) * 100 / float64(total))
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
} else if final && downloaded > 0 {
|
||||
percent = 100
|
||||
}
|
||||
|
||||
elapsed := now.Sub(start)
|
||||
if elapsed <= 0 {
|
||||
elapsed = time.Millisecond
|
||||
}
|
||||
speed := float64(downloaded) / elapsed.Seconds()
|
||||
|
||||
bar := progressBar(percent, 24)
|
||||
sizeText := humanBytes(downloaded)
|
||||
totalText := "?"
|
||||
if total > 0 {
|
||||
totalText = humanBytes(total)
|
||||
}
|
||||
line := fmt.Sprintf("\r%3d%% [%s] %s/s, %s/%s", percent, bar, humanBytesPerSecond(speed), sizeText, totalText)
|
||||
fmt.Fprint(os.Stderr, line)
|
||||
if final {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
n, readErr := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
if _, err := out.Write(buf[:n]); err != nil {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
return err
|
||||
}
|
||||
downloaded += int64(n)
|
||||
paint(false)
|
||||
}
|
||||
if readErr != nil {
|
||||
if readErr != io.EOF {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
return readErr
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
paint(true)
|
||||
return out.Sync()
|
||||
}
|
||||
|
||||
func progressBar(percent, width int) string {
|
||||
if width <= 0 {
|
||||
width = 24
|
||||
}
|
||||
if percent < 0 {
|
||||
percent = 0
|
||||
}
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
|
||||
filled := percent * width / 100
|
||||
if filled > width {
|
||||
filled = width
|
||||
}
|
||||
|
||||
return strings.Repeat("=", filled) + strings.Repeat("-", width-filled)
|
||||
}
|
||||
|
||||
func humanBytes(n int64) string {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||||
value := float64(n)
|
||||
unit := 0
|
||||
for value >= 1024 && unit < len(units)-1 {
|
||||
value /= 1024
|
||||
unit++
|
||||
}
|
||||
if unit == 0 {
|
||||
return fmt.Sprintf("%d %s", n, units[unit])
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", value, units[unit])
|
||||
}
|
||||
|
||||
func humanBytesPerSecond(n float64) string {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||||
unit := 0
|
||||
for n >= 1024 && unit < len(units)-1 {
|
||||
n /= 1024
|
||||
unit++
|
||||
}
|
||||
if unit == 0 {
|
||||
return fmt.Sprintf("%.0f %s", n, units[unit])
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", n, units[unit])
|
||||
}
|
||||
|
||||
func verifyFileSHA256(path string, expected string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Sync()
|
||||
actual := hex.EncodeToString(h.Sum(nil))
|
||||
if !strings.EqualFold(actual, expected) {
|
||||
return fmt.Errorf("checksum mismatch for %s: expected %s, got %s", filepath.Base(path), expected, actual)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stageUpdateRestart(exePath, tempPath string) error {
|
||||
@@ -139,7 +326,7 @@ func stageUpdateRestart(exePath, tempPath string) error {
|
||||
}
|
||||
|
||||
func stageUnixUpdateRestart(exePath, tempPath string) error {
|
||||
script := `target="$3"; while kill -0 "$1" 2>/dev/null; do sleep 0.2; done; mv "$2" "$target"; exec "$target"`
|
||||
script := `target="$3"; backup="${target}.bak.$$"; cp "$target" "$backup"; trap 'rm -f "$backup"' EXIT; while kill -0 "$1" 2>/dev/null; do sleep 0.2; done; mv "$2" "$target"; if "$target"; then rm -f "$backup"; else status=$?; mv "$backup" "$target"; "$target"; exit $status; fi`
|
||||
cmd := exec.Command("sh", "-c", script, "pia-qbt-autoport-updater", strconv.Itoa(os.Getpid()), tempPath, exePath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -148,10 +335,45 @@ func stageUnixUpdateRestart(exePath, tempPath string) error {
|
||||
}
|
||||
|
||||
func stageWindowsUpdateRestart(exePath, tempPath string) error {
|
||||
script := `$target=$args[2]; Wait-Process -Id $args[0] -ErrorAction SilentlyContinue; for ($i=0; $i -lt 50; $i++) { try { Move-Item -Force $args[1] $target; break } catch { Start-Sleep -Milliseconds 200 } }; Start-Process -FilePath $target -WorkingDirectory (Split-Path $target)`
|
||||
cmd := exec.Command("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script, strconv.Itoa(os.Getpid()), tempPath, exePath)
|
||||
script := fmt.Sprintf(`
|
||||
$parentPid = %d
|
||||
$tempPath = %s
|
||||
$target = %s
|
||||
$backup = "$target.bak.$PID"
|
||||
Copy-Item -Force $target $backup
|
||||
try {
|
||||
Wait-Process -Id $parentPid -ErrorAction SilentlyContinue
|
||||
for ($i = 0; $i -lt 50; $i++) {
|
||||
try {
|
||||
Move-Item -Force $tempPath $target
|
||||
break
|
||||
} catch {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
$p = Start-Process -FilePath $target -PassThru -Wait -WorkingDirectory (Split-Path $target)
|
||||
if ($p.ExitCode -ne 0) {
|
||||
Move-Item -Force $backup $target
|
||||
$p2 = Start-Process -FilePath $target -PassThru -Wait -WorkingDirectory (Split-Path $target)
|
||||
if ($p2.ExitCode -eq 0) {
|
||||
Remove-Item -Force $backup
|
||||
}
|
||||
} else {
|
||||
Remove-Item -Force $backup
|
||||
}
|
||||
} finally {
|
||||
if (Test-Path $backup) {
|
||||
Remove-Item -Force $backup
|
||||
}
|
||||
}
|
||||
`, os.Getpid(), psSingleQuote(tempPath), psSingleQuote(exePath))
|
||||
cmd := exec.Command("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func psSingleQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -108,7 +110,10 @@ func (r ReleaseInfo) assetURL() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.assetURLByName(name)
|
||||
}
|
||||
|
||||
func (r ReleaseInfo) assetURLByName(name string) (string, error) {
|
||||
for _, asset := range r.Assets {
|
||||
if asset.Name == name {
|
||||
if asset.BrowserDownloadURL == "" {
|
||||
@@ -120,3 +125,66 @@ func (r ReleaseInfo) assetURL() (string, error) {
|
||||
|
||||
return "", fmt.Errorf("release asset %s not found", name)
|
||||
}
|
||||
|
||||
func (r ReleaseInfo) checksumInfo() (string, error) {
|
||||
name, err := r.assetName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
checksumName := name + ".sha256"
|
||||
|
||||
checksumURL, err := r.assetURLByName(checksumName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expectedHash, err := fetchChecksumHash(checksumURL, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return expectedHash, nil
|
||||
}
|
||||
|
||||
func fetchChecksumHash(rawURL, assetName string) (string, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequest(http.MethodGet, rawURL, 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 request failed: %s", resp.Status)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parseChecksumPayload(string(data), assetName)
|
||||
}
|
||||
|
||||
func parseChecksumPayload(payload, assetName string) (string, error) {
|
||||
lines := strings.Split(payload, "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
hash := fields[0]
|
||||
name := fields[len(fields)-1]
|
||||
name = strings.TrimPrefix(name, "*")
|
||||
name = strings.TrimPrefix(name, "./")
|
||||
if name == assetName || name == assetName+".exe" {
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("checksum for %s not found", assetName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user