summaryrefslogtreecommitdiff
path: root/internal/sync/updater.go
diff options
context:
space:
mode:
authorSergeiEU <39683682+SergeiEU@users.noreply.github.com>2026-04-01 10:17:15 +0400
committerSergeiEU <39683682+SergeiEU@users.noreply.github.com>2026-04-01 10:17:15 +0400
commit1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 (patch)
treed8c85273ede547e03a5727bf185f5d07e87b4a08 /internal/sync/updater.go
downloadvpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.gz
vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.bz2
vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.zip
Initial importHEADmain
Diffstat (limited to 'internal/sync/updater.go')
-rw-r--r--internal/sync/updater.go159
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
+}