backup-master: сервер бэкапов, агент и восстановление VPS
Один бинарник backupctl: - server: API для агентов и restore, веб-морда (один админ), SQLite, ретеншн GFS, heavy-части, уведомления ntfy, раздача agent.sh/restore.sh; - backup/plan: сбор по каталогу категорий (files/systemd/xui/compose), горячие дампы Postgres/Mongo/MySQL, снимок x-ui.db, docker-тома, digest образов, проверка crontab, потоковая отправка tar.zst; - restore: провижининг на основе vps_installer с теми же версиями пакетов, recipes 3x-ui/nebula/sing-box нужных версий, восстановление compose с заливкой дампов и закреплением образов по digest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
1d44d067e9
53 files changed
+7278
No files matched your search
@@ -0,0 +1,80 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
settingAdminUser = "admin_user"
|
||||
settingAdminHash = "admin_hash"
|
||||
settingNtfyURL = "ntfy_url"
|
||||
settingNtfyAuth = "ntfy_auth" // токен tk_... или "user:password"
|
||||
settingNotifySuccess = "notify_success"
|
||||
)
|
||||
|
||||
// notify отправляет уведомление в ntfy (асинхронно, ошибки только в лог).
|
||||
func (s *Server) notify(title, msg, priority, tags string) {
|
||||
url := s.st.Setting(settingNtfyURL)
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
auth := s.st.Setting(settingNtfyAuth)
|
||||
go func() {
|
||||
if err := sendNtfy(url, auth, title, msg, priority, tags); err != nil {
|
||||
log.Printf("ntfy: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func sendNtfy(url, auth, title, msg, priority, tags string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(msg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// заголовки должны быть ASCII — кириллицу кодируем по RFC 2047, ntfy это понимает
|
||||
req.Header.Set("Title", mime.BEncoding.Encode("UTF-8", title))
|
||||
if priority != "" {
|
||||
req.Header.Set("Priority", priority)
|
||||
}
|
||||
if tags != "" {
|
||||
req.Header.Set("Tags", tags)
|
||||
}
|
||||
switch {
|
||||
case auth == "":
|
||||
case strings.Contains(auth, ":"):
|
||||
u, p, _ := strings.Cut(auth, ":")
|
||||
req.SetBasicAuth(u, p)
|
||||
default:
|
||||
req.Header.Set("Authorization", "Bearer "+auth)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("ответ %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func humanBytes(n int64) string {
|
||||
const unit = 1024
|
||||
if n < unit {
|
||||
return fmt.Sprintf("%d Б", n)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for m := n / unit; m >= unit; m /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cБ", float64(n)/float64(div), []rune("КМГТП")[exp])
|
||||
}
|
||||
Reference in new issue
Block a user