diff options
| author | SergeiEU <39683682+SergeiEU@users.noreply.github.com> | 2026-04-01 10:17:15 +0400 |
|---|---|---|
| committer | SergeiEU <39683682+SergeiEU@users.noreply.github.com> | 2026-04-01 10:17:15 +0400 |
| commit | 1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 (patch) | |
| tree | d8c85273ede547e03a5727bf185f5d07e87b4a08 /internal/sync/updater.go | |
| download | vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.gz vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.bz2 vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.zip | |
Diffstat (limited to 'internal/sync/updater.go')
| -rw-r--r-- | internal/sync/updater.go | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/internal/sync/updater.go b/internal/sync/updater.go new file mode 100644 index 0000000..23cbd19 --- /dev/null +++ b/internal/sync/updater.go @@ -0,0 +1,159 @@ +package sync + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" +) + +// Updater checks for and downloads client updates. +type Updater struct { + fetcher *Fetcher + currentVer string + dataDir string +} + +// NewUpdater creates an updater. +func NewUpdater(fetcher *Fetcher, currentVersion, dataDir string) *Updater { + return &Updater{ + fetcher: fetcher, + currentVer: currentVersion, + dataDir: dataDir, + } +} + +// UpdateInfo describes an available update. +type UpdateInfo struct { + Available bool `json:"available"` + Version string `json:"version"` + Changelog string `json:"changelog"` + CurrentVer string `json:"current_version"` +} + +// Check returns info about available updates. +func (u *Updater) Check() (*UpdateInfo, error) { + ver, err := u.fetcher.FetchVersion() + if err != nil { + return nil, fmt.Errorf("check update: %w", err) + } + + return &UpdateInfo{ + Available: ver.Version != u.currentVer, + Version: ver.Version, + Changelog: ver.Changelog, + CurrentVer: u.currentVer, + }, nil +} + +// Download fetches the new binary, cleans stale configs, replaces current binary and restarts. +func (u *Updater) Download() (string, error) { + ver, err := u.fetcher.FetchVersion() + if err != nil { + return "", fmt.Errorf("fetch version: %w", err) + } + + if ver.URL == "" { + suffix := "linux-amd64" + if runtime.GOOS == "windows" { + suffix = "windows-amd64.exe" + } + ver.URL = u.fetcher.baseURL + "/releases/vpnem-" + suffix + } + + client := &http.Client{Timeout: 5 * time.Minute} + resp, err := client.Get(ver.URL) + if err != nil { + return "", fmt.Errorf("download: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("download: HTTP %d", resp.StatusCode) + } + + ext := "" + if runtime.GOOS == "windows" { + ext = ".exe" + } + + newBin := filepath.Join(u.dataDir, "vpnem-update"+ext) + f, err := os.Create(newBin) + if err != nil { + return "", fmt.Errorf("create file: %w", err) + } + + if _, err := io.Copy(f, resp.Body); err != nil { + f.Close() + os.Remove(newBin) + return "", fmt.Errorf("write update: %w", err) + } + f.Close() + + if runtime.GOOS != "windows" { + os.Chmod(newBin, 0o755) + } + + // Clean stale configs so new version starts fresh + os.Remove(filepath.Join(u.dataDir, "state.json")) + os.Remove(filepath.Join(u.dataDir, "config.json")) + os.Remove(filepath.Join(u.dataDir, "cache.db")) + + // Replace current binary and restart + currentBin, _ := os.Executable() + if currentBin != "" { + if runtime.GOOS == "windows" { + // Windows can't overwrite running exe — rename old, copy new, restart + oldBin := currentBin + ".old" + os.Remove(oldBin) + os.Rename(currentBin, oldBin) + copyFile(newBin, currentBin) + // Restart: launch new binary and exit current + cmd := exec.Command(currentBin) + cmd.Dir = u.dataDir + cmd.Start() + os.Exit(0) + } else { + // Linux: overwrite in place, then re-exec + copyFile(newBin, currentBin) + os.Remove(newBin) + cmd := exec.Command(currentBin) + cmd.Dir = u.dataDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Start() + os.Exit(0) + } + } + + return newBin, nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + // Preserve executable permission on Linux + if runtime.GOOS != "windows" { + os.Chmod(dst, 0o755) + } + return nil +} |
