Files

156 lines
4.2 KiB
Go
Raw Permalink Normal View History

package updater
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
)
// ReleaseInfo enthält Informationen über die aktuell verfügbare Agent-Version.
type ReleaseInfo struct {
Version string `json:"version"`
DownloadUrl string `json:"downloadUrl"`
Checksum string `json:"checksum"` // SHA256 hex
}
// CheckForUpdate fragt den Backend-Server nach der aktuellen Version.
// serverAddress: z.B. "localhost:5000" (REST Port, nicht gRPC)
func CheckForUpdate(ctx context.Context, serverAddress, currentVersion string) (*ReleaseInfo, error) {
url := fmt.Sprintf("http://%s/api/v1/agent/releases/latest?os=%s&arch=%s",
serverAddress, runtime.GOOS, runtime.GOARCH)
httpCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching release info: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %d from release endpoint", resp.StatusCode)
}
var info ReleaseInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, fmt.Errorf("decoding release info: %w", err)
}
if info.Version == currentVersion {
return nil, nil // bereits aktuell
}
return &info, nil
}
// Update lädt die neue Version herunter und startet einen Austausch-Prozess.
// Nach erfolgreichem Download wird das neue Binary neben das aktuelle gelegt
// und ein Neustartprozess gestartet.
func Update(ctx context.Context, info *ReleaseInfo) error {
binaryPath, err := os.Executable()
if err != nil {
return fmt.Errorf("determining executable path: %w", err)
}
// Download der neuen Version
dlCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
req, err := http.NewRequestWithContext(dlCtx, http.MethodGet, info.DownloadUrl, nil)
if err != nil {
return fmt.Errorf("creating download request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("downloading new binary: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status %d during download", resp.StatusCode)
}
fileBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading download body: %w", err)
}
// SHA256-Prüfung (überspringen wenn Checksum leer = Dev-Modus)
if info.Checksum != "" {
sum := sha256.Sum256(fileBytes)
got := hex.EncodeToString(sum[:])
if got != info.Checksum {
return fmt.Errorf("checksum mismatch: expected %s, got %s", info.Checksum, got)
}
}
newBinaryPath := binaryPath + ".new"
if err := os.WriteFile(newBinaryPath, fileBytes, 0755); err != nil {
return fmt.Errorf("writing new binary: %w", err)
}
// Platform-spezifischer Neustart
if runtime.GOOS == "windows" {
return restartWindows(binaryPath, newBinaryPath)
}
return restartUnix(binaryPath, newBinaryPath)
}
func restartWindows(binaryPath, newBinaryPath string) error {
batchContent := fmt.Sprintf(`@echo off
timeout /t 2 /nobreak > nul
move /y "%s" "%s"
start "" "%s"
del "%%~f0"
`, newBinaryPath, binaryPath, binaryPath)
batchPath := filepath.Join(os.TempDir(), "nexus-agent-update.bat")
if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil {
return fmt.Errorf("writing update batch script: %w", err)
}
if err := exec.Command("cmd", "/c", "start", "", batchPath).Start(); err != nil {
return fmt.Errorf("starting update batch script: %w", err)
}
os.Exit(0)
return nil
}
func restartUnix(binaryPath, newBinaryPath string) error {
scriptContent := fmt.Sprintf(`#!/bin/sh
sleep 2
mv -f "%s" "%s"
"%s" &
rm -f "$0"
`, newBinaryPath, binaryPath, binaryPath)
scriptPath := filepath.Join(os.TempDir(), "nexus-agent-update.sh")
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil {
return fmt.Errorf("writing update shell script: %w", err)
}
if err := exec.Command("sh", scriptPath).Start(); err != nil {
return fmt.Errorf("starting update shell script: %w", err)
}
os.Exit(0)
return nil
}