package api import ( "fmt" "io" "net/http" "os" "github.com/mrleo1nid/pzmanager/internal/config" "github.com/mrleo1nid/pzmanager/internal/pzserver" ) // profileDTO — профиль в том виде, в каком его показывает панель. Пароль // наружу не отдаётся, только признак того, что он задан. type profileDTO struct { ID string `json:"id"` Title string `json:"title"` JavaMemory string `json:"java_memory"` ExtraArgs []string `json:"extra_args"` HasAdminPassword bool `json:"has_admin_password"` AdminPassword string `json:"admin_password,omitempty"` // Признаки состояния на диске: по ним панель подсказывает, что профиль // ещё ни разу не запускался. HasConfig bool `json:"has_config"` WorldReady bool `json:"world_ready"` Active bool `json:"active"` // CopyFrom заполняется только при создании профиля. CopyFrom string `json:"copy_from,omitempty"` } func (s *Server) profileToDTO(p config.Profile) profileDTO { return profileDTO{ ID: p.ID, Title: p.Title, JavaMemory: p.JavaMemory, ExtraArgs: p.ExtraArgs, HasAdminPassword: p.AdminPassword != "", HasConfig: fileExists(s.cfg.ServerConfigPathFor(p.ID)), WorldReady: fileExists(s.cfg.WorldDirFor(p.ID)), Active: p.ID == s.cfg.Active, } } func (s *Server) handleListProfiles(w http.ResponseWriter, r *http.Request) { profiles := make([]profileDTO, 0, len(s.cfg.Profiles)) for _, p := range s.cfg.Profiles { profiles = append(profiles, s.profileToDTO(p)) } writeJSON(w, http.StatusOK, map[string]any{ "profiles": profiles, "active": s.cfg.Active, // Переключать профиль можно только на остановленном сервере. "can_switch": s.sup.State() == pzserver.StateStopped, }) } // handleCreateProfile заводит новый профиль и, если попросили, копирует в него // конфиг и настройки песочницы существующего. func (s *Server) handleCreateProfile(w http.ResponseWriter, r *http.Request) { var dto profileDTO if !decodeJSON(w, r, &dto) { return } profile := config.Profile{ ID: dto.ID, Title: dto.Title, JavaMemory: dto.JavaMemory, ExtraArgs: dto.ExtraArgs, AdminPassword: dto.AdminPassword, } if err := s.cfg.AddProfile(profile); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if dto.CopyFrom != "" { if err := s.copyProfileConfigs(dto.CopyFrom, profile.ID); err != nil { // Профиль уже добавлен в память — откатываем, чтобы не остался // наполовину настроенный. _ = s.cfg.DeleteProfile(profile.ID) writeError(w, http.StatusBadRequest, err.Error()) return } } if err := s.cfg.Save(); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.sup.AppendLog("manager", fmt.Sprintf("Создан профиль %q", profile.Name())) writeJSON(w, http.StatusCreated, s.profileToDTO(profile)) } // copyProfileConfigs дублирует .ini и SandboxVars.lua исходного профиля под // именем нового: так новый сервер стартует с уже знакомыми настройками и // списком модов, но со своим миром. func (s *Server) copyProfileConfigs(from, to string) error { if _, ok := s.cfg.Profile(from); !ok { return fmt.Errorf("профиль-источник %q не найден", from) } pairs := [][2]string{ {s.cfg.ServerConfigPathFor(from), s.cfg.ServerConfigPathFor(to)}, {s.cfg.SandboxConfigPathFor(from), s.cfg.SandboxConfigPathFor(to)}, } copied := 0 for _, pair := range pairs { err := copyFile(pair[0], pair[1]) if os.IsNotExist(err) { continue } if err != nil { return fmt.Errorf("скопировать настройки: %w", err) } copied++ } if copied == 0 { return fmt.Errorf("у профиля %q ещё нет файлов настроек — сначала запустите его хотя бы раз", from) } s.sup.AppendLog("manager", fmt.Sprintf("Настройки профиля %q скопированы в %q", from, to)) return nil } func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") current, ok := s.cfg.Profile(id) if !ok { writeError(w, http.StatusNotFound, "профиль не найден") return } var dto profileDTO if !decodeJSON(w, r, &dto) { return } updated := config.Profile{ ID: id, Title: dto.Title, JavaMemory: dto.JavaMemory, ExtraArgs: dto.ExtraArgs, // Пустой пароль означает "оставить прежний": наружу мы его не отдаём, // и форма присылает пустую строку, пока его не меняли. AdminPassword: current.AdminPassword, } if dto.AdminPassword != "" { updated.AdminPassword = dto.AdminPassword } if err := s.cfg.UpdateProfile(id, updated); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := s.cfg.Save(); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } note := "Профиль сохранён" if s.sup.State() != pzserver.StateStopped && id == s.cfg.Active { note = "Профиль сохранён. Память и аргументы запуска применятся после перезапуска сервера." } writeJSON(w, http.StatusOK, map[string]any{"saved": true, "note": note}) } func (s *Server) handleDeleteProfile(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := s.cfg.DeleteProfile(id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := s.cfg.Save(); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.sup.AppendLog("manager", fmt.Sprintf( "Профиль %q удалён из панели. Мир и конфиги остались на диске: %s", id, s.cfg.WorldDirFor(id))) writeJSON(w, http.StatusOK, map[string]any{"deleted": id}) } // handleActivateProfile переключает активный профиль. Делать это на живом // сервере нельзя: профили делят игровые порты и файлы запуска. func (s *Server) handleActivateProfile(w http.ResponseWriter, r *http.Request) { if s.sup.State() != pzserver.StateStopped { writeError(w, http.StatusConflict, "сначала остановите игровой сервер") return } id := r.PathValue("id") if err := s.cfg.SetActive(id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := s.cfg.Save(); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.sup.AppendLog("manager", fmt.Sprintf("Активный профиль: %s", s.cfg.ActiveProfile().Name())) writeJSON(w, http.StatusOK, map[string]any{"active": id}) } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() // O_EXCL: перезаписывать чужой конфиг при создании профиля нельзя. out, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err } func fileExists(p string) bool { _, err := os.Stat(p) return err == nil }