CI / test (push) Successful in 1m29s
- сервер отдаёт агенту итоговый каталог (встроенный каталог сервера + переопределения клиента); если каталог не подходит версии агента — встроенный каталог агента и предупреждение - самообновление перед бэкапом: версия сервера новее → скачивание с того же сервера (только /dl/...), проверка sha256 и запуска новой версии, подмена с откатом, перезапуск через exec; при ошибке бэкап идёт текущей версией - галочка «Автообновление» у клиента (по умолчанию включена), версия агента в списке и карточке, отставшие подсвечены; версия сервера в шапке - миграция существующей базы: колонки auto_update и agent_version - backupctl backup --no-update Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.hsrv.site/mrleo1nid/backup-master/internal/api"
|
|
)
|
|
|
|
// updatedEnv выставляется перезапущенному процессу: не обновляться повторно в том же запуске.
|
|
const updatedEnv = "BM_UPDATED_FROM"
|
|
|
|
// selfUpdate скачивает новую версию агента с того же сервера, сверяет sha256,
|
|
// подменяет бинарник (старый сохраняется для отката) и перезапускает процесс.
|
|
// При успехе не возвращается.
|
|
func selfUpdate(ctx context.Context, client *api.Client, u *api.AgentUpdate, current string) error {
|
|
// только путь на своём сервере: чужой адрес от сервера не принимаем
|
|
if !strings.HasPrefix(u.Path, "/dl/") || strings.Contains(u.Path, "..") || strings.Contains(u.Path, "://") {
|
|
return fmt.Errorf("недопустимый путь обновления %q", u.Path)
|
|
}
|
|
if len(u.SHA256) != 64 {
|
|
return errors.New("сервер не прислал sha256")
|
|
}
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exe, err = filepath.EvalSymlinks(exe); err != nil {
|
|
return err
|
|
}
|
|
|
|
dctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
|
defer cancel()
|
|
body, err := client.Download(dctx, u.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("скачивание: %w", err)
|
|
}
|
|
defer body.Close()
|
|
|
|
tmp := exe + ".new"
|
|
if err := writeVerified(tmp, body, u.SHA256); err != nil {
|
|
os.Remove(tmp)
|
|
return err
|
|
}
|
|
// новый бинарник должен запускаться и сообщать ожидаемую версию
|
|
if err := checkVersion(ctx, tmp, u.Version); err != nil {
|
|
os.Remove(tmp)
|
|
return err
|
|
}
|
|
|
|
old := exe + ".old"
|
|
if err := os.Rename(exe, old); err != nil {
|
|
os.Remove(tmp)
|
|
return err
|
|
}
|
|
if err := os.Rename(tmp, exe); err != nil {
|
|
_ = os.Rename(old, exe)
|
|
return err
|
|
}
|
|
env := append(os.Environ(), updatedEnv+"="+current)
|
|
err = reexec(exe, os.Args, env)
|
|
// сюда попадаем, только если exec не удался — возвращаем старую версию
|
|
_ = os.Rename(old, exe)
|
|
return fmt.Errorf("перезапуск: %w", err)
|
|
}
|
|
|
|
func writeVerified(path string, r io.Reader, want string) error {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h := sha256.New()
|
|
if _, err := io.Copy(io.MultiWriter(f, h), r); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return err
|
|
}
|
|
if got := hex.EncodeToString(h.Sum(nil)); got != want {
|
|
return fmt.Errorf("sha256 не совпал: %s, ожидался %s", got, want)
|
|
}
|
|
return os.Chmod(path, 0o755)
|
|
}
|
|
|
|
func checkVersion(ctx context.Context, bin, want string) error {
|
|
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
out, err := exec.CommandContext(cctx, bin, "version").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("новый бинарник не запускается: %w", err)
|
|
}
|
|
if !strings.Contains(string(out), want) {
|
|
return fmt.Errorf("новый бинарник сообщает версию %q, ожидалась %s", strings.TrimSpace(string(out)), want)
|
|
}
|
|
return nil
|
|
}
|