package api import ( "context" "errors" "fmt" "net/http" "os" "time" "github.com/mrleo1nid/pzmanager/internal/job" "github.com/mrleo1nid/pzmanager/internal/pzconfig" "github.com/mrleo1nid/pzmanager/internal/pzserver" ) func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, s.jobs.Status()) } func (s *Server) handleTaskCancel(w http.ResponseWriter, r *http.Request) { if !s.jobs.Busy() { writeError(w, http.StatusConflict, "нет активной задачи") return } s.jobs.Cancel() writeJSON(w, http.StatusOK, map[string]any{"cancelled": true}) } // handleInstall ставит или обновляет серверные файлы через SteamCMD. Обновлять // файлы под работающим сервером нельзя: SteamCMD перезапишет то, что открыто // JVM, и сервер упадёт непредсказуемо. func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) { if s.sup.State() != pzserver.StateStopped { writeError(w, http.StatusConflict, "сначала остановите игровой сервер") return } name := "Установка/обновление сервера через SteamCMD" err := s.jobs.Start(name, func(ctx context.Context, log func(string)) error { if err := s.steam.InstallOrUpdate(ctx, log, s.cfg.ServerDir); err != nil { return err } if err := s.applyJavaMemory(log); err != nil { log("Не удалось выставить объём памяти JVM: " + err.Error()) } return nil }) if err != nil { writeOpError(w, err) return } writeJSON(w, http.StatusAccepted, map[string]any{"task": name}) } func (s *Server) handleListBackups(w http.ResponseWriter, r *http.Request) { archives, err := s.backups.List() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"backups": archives, "dir": s.backups.Dir()}) } func (s *Server) handleCreateBackup(w http.ResponseWriter, r *http.Request) { var body struct { Note string `json:"note"` } if r.ContentLength > 0 && !decodeJSON(w, r, &body) { return } name := "Резервное копирование мира" err := s.jobs.Start(name, func(ctx context.Context, log func(string)) error { // Опциональная остановка сервера даёт согласованный снимок: PZ // сбрасывает мир на диск только при сохранении. restart := false if s.cfg.Backup.StopServer && s.sup.State() != pzserver.StateStopped { log("Останавливаю сервер перед бэкапом") if err := s.sup.Stop(); err != nil && !errors.Is(err, pzserver.ErrNotRunning) { return err } restart = true } _, err := s.backups.Create(ctx, log, body.Note) if restart { log("Поднимаю сервер обратно") if startErr := s.sup.Start(); startErr != nil { log("Не удалось запустить сервер: " + startErr.Error()) } } return err }) if err != nil { writeOpError(w, err) return } writeJSON(w, http.StatusAccepted, map[string]any{"task": name}) } func (s *Server) handleDeleteBackup(w http.ResponseWriter, r *http.Request) { if err := s.backups.Delete(r.PathValue("name")); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"deleted": r.PathValue("name")}) } func (s *Server) handleDownloadBackup(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") path, err := s.backups.Path(name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } f, err := os.Open(path) if err != nil { writeError(w, http.StatusNotFound, "архив не найден") return } defer f.Close() info, err := f.Stat() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/gzip") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", name)) http.ServeContent(w, r, name, info.ModTime(), f) } // handleRestoreBackup разворачивает архив поверх текущего мира. Сервер должен // быть остановлен — иначе он держит файлы мира открытыми и перезапишет их // своим состоянием при следующем сохранении. func (s *Server) handleRestoreBackup(w http.ResponseWriter, r *http.Request) { if s.sup.State() != pzserver.StateStopped { writeError(w, http.StatusConflict, "сначала остановите игровой сервер") return } name := r.PathValue("name") if _, err := s.backups.Path(name); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } taskName := "Восстановление из архива " + name err := s.jobs.Start(taskName, func(ctx context.Context, log func(string)) error { // Перед перезаписью снимаем страховочную копию текущего состояния: // восстановление не туда — самая дорогая ошибка в этой панели. log("Делаю страховочный бэкап текущего мира") if _, err := s.backups.Create(ctx, log, "pre-restore"); err != nil { return fmt.Errorf("страховочный бэкап не создан, восстановление отменено: %w", err) } return s.backups.Restore(ctx, log, name) }) if err != nil { writeOpError(w, err) return } writeJSON(w, http.StatusAccepted, map[string]any{"task": taskName}) } // StartBackupScheduler запускает периодические бэкапы согласно конфигу. // Интервал перечитывается на каждом тике, поэтому смена расписания в панели // действует без перезапуска менеджера. func StartBackupScheduler(ctx context.Context, s *Server) { go func() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() last := time.Now() for { select { case <-ctx.Done(): return case now := <-ticker.C: interval := s.cfg.Backup.Schedule.Duration() if interval <= 0 || now.Sub(last) < interval { continue } last = now if s.jobs.Busy() { s.sup.AppendLog("manager", "Плановый бэкап пропущен: выполняется другая задача") continue } err := s.jobs.Start("Плановое резервное копирование", func(ctx context.Context, log func(string)) error { _, err := s.backups.Create(ctx, log, "auto") return err }) if err != nil && !errors.Is(err, job.ErrBusy) { s.sup.AppendLog("manager", "Плановый бэкап не запущен: "+err.Error()) } } } }() } // applyJavaMemory переносит лимит памяти из конфига менеджера в файл запуска // сервера. Вызывается после установки: SteamCMD перезаписывает этот файл. func (s *Server) applyJavaMemory(log func(string)) error { if s.cfg.JavaMemory == "" { return nil } if err := pzconfig.SetJavaMemory(s.cfg.ServerDir, s.cfg.JavaMemory); err != nil { return err } log("Лимит памяти JVM выставлен: " + s.cfg.JavaMemory) return nil }