Добавил конвенции для конфигурации и сделал рефакторинг кода

This commit is contained in:
av
2026-06-28 20:53:10 +03:00
parent 9cfccc7b4a
commit 84ffe0733e
11 changed files with 371 additions and 55 deletions
+84
View File
@@ -5,11 +5,16 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/pelletier/go-toml/v2"
)
// DefaultPath — имя конфига по умолчанию: ищется в рабочей директории
// процесса. Переопределяется опцией --config=path.
const DefaultPath = "config.toml"
// Config — корневая конфигурация сервиса (см. config.example.toml).
type Config struct {
QBittorrent QBittorrent `toml:"qbittorrent"`
@@ -195,7 +200,15 @@ func Load(path string) (*Config, error) {
return cfg, nil
}
// validate — fail-fast проверка конфига на старте: обязательные поля заданы,
// медиа-пути доступны и не выходят из песочницы, диапазоны соблюдены, секреты
// включённых секций не пусты. Длительности уже провалидированы при разборе
// TOML (UnmarshalText). Лог об ошибке пишет граница (cmd/jellybit), не загрузчик.
func (c *Config) validate() error {
// Обязательные поля ядра.
if c.QBittorrent.URL == "" {
return errors.New("qbittorrent.url is empty")
}
if c.HTTP.Listen == "" {
return errors.New("http.listen is empty")
}
@@ -205,5 +218,76 @@ func (c *Config) validate() error {
if c.LLM.Type != "openai-compat" {
return fmt.Errorf("unsupported llm.type %q (supported: openai-compat)", c.LLM.Type)
}
// Медиа-пути песочницы: абсолютные, без traversal, существующие каталоги.
for _, p := range []struct{ name, path string }{
{"paths.downloads", c.Paths.Downloads},
{"paths.movies", c.Paths.Movies},
{"paths.series", c.Paths.Series},
} {
if err := validateMediaDir(p.name, p.path); err != nil {
return err
}
}
// Диапазоны.
if t := c.Recognition.AutoConfidenceThreshold; t < 0 || t > 1 {
return fmt.Errorf("recognition.auto_confidence_threshold %.3f is out of range [0, 1]", t)
}
if c.LLM.MaxRetries < 0 {
return fmt.Errorf("llm.max_retries %d must be >= 0", c.LLM.MaxRetries)
}
// Обязательные секреты включённых секций (ловит криво отрендеренный деплоем
// файл). qBittorrent — ядро, пароль нужен всегда.
if c.QBittorrent.Password == "" {
return errors.New("qbittorrent.password is empty (required secret)")
}
// llm.api_key намеренно не обязателен: keyless-local LLM (LM Studio с
// заданным base_url, но без ключа) — валидный документированный дефолт.
// Консистентность опциональных секций: enabled ⇒ заданы нужные поля/секреты.
if c.Metadata.TMDB.Enabled && c.Metadata.TMDB.APIKey == "" {
return errors.New("metadata.tmdb.enabled but metadata.tmdb.api_key is empty")
}
if c.Metadata.TVDB.Enabled && c.Metadata.TVDB.APIKey == "" {
return errors.New("metadata.tvdb.enabled but metadata.tvdb.api_key is empty")
}
if c.Jellyfin.Enabled {
if c.Jellyfin.URL == "" {
return errors.New("jellyfin.enabled but jellyfin.url is empty")
}
if c.Jellyfin.APIKey == "" {
return errors.New("jellyfin.enabled but jellyfin.api_key is empty (required secret)")
}
}
if c.Telegram.Enabled && c.Telegram.Token == "" {
return errors.New("telegram.enabled but telegram.token is empty (required secret)")
}
return nil
}
// validateMediaDir проверяет путь медиа-песочницы: непустой, абсолютный, без
// traversal (filepath.Clean — без `..`/лишних разделителей) и указывает на
// существующий доступный каталог. Отдельного корня песочницы в конфиге нет,
// поэтому «строго под песочницей» обеспечиваем абсолютностью и отсутствием
// traversal; единый монтируемый корень (/srv/media) — забота деплоя.
func validateMediaDir(name, path string) error {
if path == "" {
return fmt.Errorf("%s is empty", name)
}
if !filepath.IsAbs(path) {
return fmt.Errorf("%s %q must be an absolute path", name, path)
}
if filepath.Clean(path) != path {
return fmt.Errorf("%s %q must be a clean path (no .. or redundant separators)", name, path)
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s %q is not accessible: %w", name, path, err)
}
if !info.IsDir() {
return fmt.Errorf("%s %q is not a directory", name, path)
}
return nil
}