fix: пути (db, worktree) и config.yaml резолвятся рядом с .exe, а не от CWD
This commit is contained in:
@@ -55,6 +55,9 @@ func New(configPath, version, updateToken string) (*App, error) {
|
|||||||
return nil, fmt.Errorf("%w: %v", ErrConfig, err)
|
return nil, fmt.Errorf("%w: %v", ErrConfig, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Относительные пути (db, worktree) — рядом с .exe, а не от CWD запуска.
|
||||||
|
cfg.ResolveExePaths()
|
||||||
|
|
||||||
// SQLite для задач и трасс
|
// SQLite для задач и трасс
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -155,6 +156,58 @@ func TestLoad_FileNotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveExePaths_RelativeToExe(t *testing.T) {
|
||||||
|
t.Setenv("TG_TOKEN", "tok")
|
||||||
|
t.Setenv("TG_CHAT_ID", "42")
|
||||||
|
|
||||||
|
yaml := `telegram:
|
||||||
|
token: "${TG_TOKEN}"
|
||||||
|
chat_id: "${TG_CHAT_ID}"
|
||||||
|
paths:
|
||||||
|
db: "./ratatoskr.db"
|
||||||
|
worktree: "./worktrees"
|
||||||
|
`
|
||||||
|
cfg, err := Load(writeCfg(t, yaml))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
cfg.ResolveExePaths()
|
||||||
|
|
||||||
|
base := ExeDir()
|
||||||
|
if cfg.Paths.DB != filepath.Join(base, "ratatoskr.db") {
|
||||||
|
t.Errorf("db = %q, want %q", cfg.Paths.DB, filepath.Join(base, "ratatoskr.db"))
|
||||||
|
}
|
||||||
|
if cfg.Paths.Worktree != filepath.Join(base, "worktrees") {
|
||||||
|
t.Errorf("worktree = %q, want %q", cfg.Paths.Worktree, filepath.Join(base, "worktrees"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveExePaths_AbsoluteKept(t *testing.T) {
|
||||||
|
t.Setenv("TG_TOKEN", "tok")
|
||||||
|
t.Setenv("TG_CHAT_ID", "42")
|
||||||
|
|
||||||
|
absDB := filepath.Join(string(filepath.Separator), "data", "ratatoskr.db") // абсолютный для текущей ОС
|
||||||
|
absWt := filepath.Join(string(filepath.Separator), "worktrees")
|
||||||
|
yaml := `telegram:
|
||||||
|
username: "${TG_TOKEN}"
|
||||||
|
chat_id: "${TG_CHAT_ID}"
|
||||||
|
paths:
|
||||||
|
db: "` + absDB + `"
|
||||||
|
worktree: "` + absWt + `"
|
||||||
|
`
|
||||||
|
cfg, err := Load(writeCfg(t, yaml))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
cfg.ResolveExePaths()
|
||||||
|
if cfg.Paths.DB != absDB {
|
||||||
|
t.Errorf("db = %q, want абсолютный путь сохранён %q", cfg.Paths.DB, absDB)
|
||||||
|
}
|
||||||
|
if cfg.Paths.Worktree != absWt {
|
||||||
|
t.Errorf("worktree = %q, want абсолютный путь сохранён %q", cfg.Paths.Worktree, absWt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoad_MissingRequired(t *testing.T) {
|
func TestLoad_MissingRequired(t *testing.T) {
|
||||||
// пустой конфиг — все обязательные поля отсутствуют
|
// пустой конфиг — все обязательные поля отсутствуют
|
||||||
t.Setenv("TG_TOKEN", "")
|
t.Setenv("TG_TOKEN", "")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,11 +12,13 @@ import (
|
|||||||
|
|
||||||
// Load читает конфиг из yaml-файла, подставляет ${VAR:-default}, накладывает
|
// Load читает конфиг из yaml-файла, подставляет ${VAR:-default}, накладывает
|
||||||
// env-переменные (тег env) и валидирует. Если path пустой — ищет config.yaml
|
// env-переменные (тег env) и валидирует. Если path пустой — ищет config.yaml
|
||||||
// в CWD. Ошибка C4 если файл не существует (но не fatal при path="").
|
// сначала в каталоге бинаря (exeDir), затем в CWD (fallback). Ошибка C4 если
|
||||||
|
// файл не существует (но не fatal при path="").
|
||||||
func Load(path string) (*Config, error) {
|
func Load(path string) (*Config, error) {
|
||||||
noFile := false
|
noFile := false
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "config.yaml"
|
// приоритет — рядом с .exe, фоллбэк на CWD
|
||||||
|
path = firstExistingPath(filepath.Join(ExeDir(), "config.yaml"), "config.yaml")
|
||||||
}
|
}
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,6 +50,40 @@ func Load(path string) (*Config, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExeDir возвращает каталог исполняемого файла (бинаря). Используется для
|
||||||
|
// поиска config.yaml и резолва относительных путей "рядом с .exe".
|
||||||
|
func ExeDir() string {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return filepath.Dir(exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstExistingPath возвращает первый существующий путь из списка.
|
||||||
|
func firstExistingPath(paths ...string) string {
|
||||||
|
for _, p := range paths {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths[len(paths)-1] // фоллбэк на последний
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveExePaths подставляет относительные пути ПУТЕЙ (db, worktree) к каталогу
|
||||||
|
// бинаря, чтобы они были стабильны независимо от CWD запуска. Абсолютные пути
|
||||||
|
// не трогает. Вызывается после Load (в app.New), а не внутри Load, чтобы
|
||||||
|
// персист-тесты дефолтов оставались независимыми от окружения.
|
||||||
|
func (c *Config) ResolveExePaths() {
|
||||||
|
base := ExeDir()
|
||||||
|
if c.Paths.DB != "" && !filepath.IsAbs(c.Paths.DB) {
|
||||||
|
c.Paths.DB = filepath.Join(base, c.Paths.DB)
|
||||||
|
}
|
||||||
|
if c.Paths.Worktree != "" && !filepath.IsAbs(c.Paths.Worktree) {
|
||||||
|
c.Paths.Worktree = filepath.Join(base, c.Paths.Worktree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// envLookup обрабатывает ${VAR:-default} в YAML.
|
// envLookup обрабатывает ${VAR:-default} в YAML.
|
||||||
func envLookup(key string) string {
|
func envLookup(key string) string {
|
||||||
parts := strings.SplitN(key, ":-", 2)
|
parts := strings.SplitN(key, ":-", 2)
|
||||||
|
|||||||
Reference in New Issue
Block a user