Оповещения: группа «Администратор системы», блок «Кому писать» открыт; копии: адрес хранилища как в проводнике

Администратору (адрес из «Почты») — сбои (ИШ, связь с НРД, службы bj, DEPO) и обновления (новый выпуск bj,
ИШ, лицензия, сертификаты); отделам — переводы и поручения. Существующие установки: администратор получает
всё, что получал. Новые оповещения: «Доступно обновление bj» (раз на выпуск), «Служба … не работает /
снова работает» (модули и служба ключей Валидаты). Резервные копии: \\сервер\папка или сервер + общая
папка по полям → //сервер/папка; понятная причина отказа доступа.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 22:44:35 +03:00
parent f07bb53162
commit d23ddf6e0c
16 changed files with 547 additions and 63 deletions
+55 -3
View File
@@ -46,6 +46,49 @@ func (b BackupSettings) keep() int {
func (b BackupSettings) dir() string { return pickStr(strings.TrimSpace(b.Dir), "bj-backup") }
// smbLocation — адрес хранилища, как его вводят люди (\\nas\backup\bj как в проводнике, //nas/backup, smb://nas/backup;
// или сервер в поле «Хранилище», а общая папка — в поле «Папка»), → общая папка для smbclient (//сервер/папка) и
// путь внутри неё. Раньше «\\10.10.10.11\» + «backup_test» превращалось в //10.10.10.11//backup_test — без общей
// папки, и доступ не проверялся. Только сервер без общей папки — share «//сервер» (форма скажет, чего не хватает).
func smbLocation(share, dir string) (string, string) {
parts := func(s string) []string {
s = strings.ReplaceAll(strings.TrimSpace(s), `\`, "/")
if i := strings.Index(s, "://"); i > 0 && strings.EqualFold(s[:i], "smb") {
s = s[i+3:]
}
var out []string
for _, p := range strings.Split(s, "/") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
sp, dp := parts(share), parts(dir)
if len(sp) == 0 {
return "", strings.Join(dp, "/")
}
if len(sp) == 1 { // только сервер: общая папка — начало «Папки»
if len(dp) == 0 {
return "//" + sp[0], ""
}
sp, dp = []string{sp[0], dp[0]}, dp[1:]
}
sub := append(append([]string{}, sp[2:]...), dp...)
return "//" + sp[0] + "/" + sp[1], strings.Join(sub, "/")
}
// smbWin — адрес хранилища так, как его видят в проводнике Windows (//nas/backup → \\nas\backup).
func smbWin(share string) string {
if !strings.HasPrefix(share, "//") {
return share
}
return strings.ReplaceAll(share, "/", `\`)
}
// smbShareOK — в адресе есть и сервер, и общая папка.
func smbShareOK(share string) bool { return strings.Count(strings.TrimPrefix(share, "//"), "/") == 1 }
// BackupStatus — итог последней копии (пишет root-скрипт).
type BackupStatus struct {
At string `json:"at"`
@@ -85,6 +128,12 @@ func backupParams(r *http.Request) map[string]string {
p[k] = v
}
}
if p["share"] != "" { // адрес как в проводнике → общая папка и путь внутри неё
p["share"], p["dir"] = smbLocation(p["share"], p["dir"])
if p["dir"] == "" {
delete(p, "dir")
}
}
return p
}
@@ -110,14 +159,17 @@ func registerBackup(mux *http.ServeMux, rc *RuntimeConfig) {
}
b := rc.Snapshot().Backup
b.Enabled = r.FormValue("enabled") != ""
b.Share = strings.TrimSpace(r.FormValue("share"))
b.Dir = strings.TrimSpace(r.FormValue("dir"))
b.Share, b.Dir = smbLocation(r.FormValue("share"), r.FormValue("dir"))
b.User = strings.TrimSpace(r.FormValue("user"))
b.EveryHours, _ = strconv.Atoi(r.FormValue("every_hours"))
b.KeepDays, _ = strconv.Atoi(r.FormValue("keep_days"))
pass := r.FormValue("password")
if b.Share == "" {
setupFlash(w, r, "Резервное копирование: укажите хранилище, например //nas/backup")
setupFlash(w, r, `Резервное копирование: укажите сетевую папку, как в проводнике, например \\nas\backup`)
return
}
if !smbShareOK(b.Share) {
setupFlash(w, r, `Резервное копирование: укажите не только сервер, но и общую папку на нём — например \\`+strings.TrimPrefix(b.Share, "//")+`\backup`)
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)