Найдено статическим анализом (dupl/jscpd) + проверено вручную:
Go:
- appstate/path.go: ResolveDBPath/ResolveConfigPath → общий resolvePath()
- handlers/respond.go: дженерик BindJSON[T] для bind+422-ошибки
- article/generator.go: buildNetSegment/buildPSUSegment → buildProfileSegment()
- cmd/qfs/main.go (+respond.go): 8 повторяющихся switch{case errors.Is(...)}
→ respondByErrCase(c, err, errCase{...}, ...)
Frontend (index.html):
- 5 идентичных обработчиков клавиатурной навигации автокомплита
→ handleAutocompleteKeyGeneric(event, onSelect)
- 4 функции сборки нового элемента корзины из автокомплита
→ buildNewCartItem()/commitCartChange()
Также: PricingMarkup — единая точка правды для аплифт-коэффициента
в JS (render + export), с cross-reference комментариями к Go-константам
в export.go (defaultSaleMarkup/stockCompetitorMarkupFactor).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
211 lines
5.0 KiB
Go
211 lines
5.0 KiB
Go
package appstate
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
appDirName = "QuoteForge"
|
|
defaultDB = "qfs.db"
|
|
defaultCfg = "config.yaml"
|
|
envDBPath = "QFS_DB_PATH"
|
|
envStateDir = "QFS_STATE_DIR"
|
|
envCfgPath = "QFS_CONFIG_PATH"
|
|
)
|
|
|
|
// ResolveDBPath returns the local SQLite path using priority:
|
|
// explicit CLI path > QFS_DB_PATH > OS-specific user state directory.
|
|
func ResolveDBPath(explicitPath string) (string, error) {
|
|
return resolvePath(explicitPath, envDBPath, defaultDB)
|
|
}
|
|
|
|
// ResolveConfigPath returns the config path using priority:
|
|
// explicit CLI path > QFS_CONFIG_PATH > OS-specific user state directory.
|
|
func ResolveConfigPath(explicitPath string) (string, error) {
|
|
return resolvePath(explicitPath, envCfgPath, defaultCfg)
|
|
}
|
|
|
|
// resolvePath returns a path using priority: explicit CLI path > envVar > OS-specific
|
|
// user state directory joined with defaultFilename.
|
|
func resolvePath(explicitPath, envVar, defaultFilename string) (string, error) {
|
|
if explicitPath != "" {
|
|
return filepath.Clean(explicitPath), nil
|
|
}
|
|
|
|
if fromEnv := os.Getenv(envVar); fromEnv != "" {
|
|
return filepath.Clean(fromEnv), nil
|
|
}
|
|
|
|
dir, err := defaultStateDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(dir, defaultFilename), nil
|
|
}
|
|
|
|
// ResolveConfigPathNearDB returns config path using priority:
|
|
// explicit CLI path > QFS_CONFIG_PATH > directory of resolved local DB path.
|
|
// Falls back to ResolveConfigPath when dbPath is empty.
|
|
func ResolveConfigPathNearDB(explicitPath, dbPath string) (string, error) {
|
|
if explicitPath != "" {
|
|
return filepath.Clean(explicitPath), nil
|
|
}
|
|
|
|
if fromEnv := os.Getenv(envCfgPath); fromEnv != "" {
|
|
return filepath.Clean(fromEnv), nil
|
|
}
|
|
|
|
if strings.TrimSpace(dbPath) != "" {
|
|
return filepath.Join(filepath.Dir(filepath.Clean(dbPath)), defaultCfg), nil
|
|
}
|
|
|
|
return ResolveConfigPath("")
|
|
}
|
|
|
|
// MigrateLegacyDB copies an existing legacy DB (and optional SQLite sidecars)
|
|
// to targetPath if targetPath does not already exist.
|
|
// Returns source path if migration happened.
|
|
func MigrateLegacyDB(targetPath string, legacyPaths []string) (string, error) {
|
|
if targetPath == "" {
|
|
return "", nil
|
|
}
|
|
|
|
if exists(targetPath) {
|
|
return "", nil
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
return "", fmt.Errorf("creating target db directory: %w", err)
|
|
}
|
|
|
|
for _, src := range legacyPaths {
|
|
if src == "" {
|
|
continue
|
|
}
|
|
src = filepath.Clean(src)
|
|
if src == targetPath || !exists(src) {
|
|
continue
|
|
}
|
|
|
|
if err := copyFile(src, targetPath); err != nil {
|
|
return "", fmt.Errorf("migrating legacy db from %s: %w", src, err)
|
|
}
|
|
|
|
// Optional SQLite sidecar files.
|
|
_ = copyIfExists(src+"-wal", targetPath+"-wal")
|
|
_ = copyIfExists(src+"-shm", targetPath+"-shm")
|
|
|
|
return src, nil
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
// MigrateLegacyFile copies an existing legacy file to targetPath
|
|
// if targetPath does not already exist.
|
|
func MigrateLegacyFile(targetPath string, legacyPaths []string) (string, error) {
|
|
if targetPath == "" {
|
|
return "", nil
|
|
}
|
|
|
|
if exists(targetPath) {
|
|
return "", nil
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
return "", fmt.Errorf("creating target directory: %w", err)
|
|
}
|
|
|
|
for _, src := range legacyPaths {
|
|
if src == "" {
|
|
continue
|
|
}
|
|
src = filepath.Clean(src)
|
|
if src == targetPath || !exists(src) {
|
|
continue
|
|
}
|
|
if err := copyFile(src, targetPath); err != nil {
|
|
return "", fmt.Errorf("migrating legacy file from %s: %w", src, err)
|
|
}
|
|
return src, nil
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func defaultStateDir() (string, error) {
|
|
if override := os.Getenv(envStateDir); override != "" {
|
|
return filepath.Clean(override), nil
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
base, err := os.UserConfigDir() // ~/Library/Application Support
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolving user config dir: %w", err)
|
|
}
|
|
return filepath.Join(base, appDirName), nil
|
|
case "windows":
|
|
if local := os.Getenv("LOCALAPPDATA"); local != "" {
|
|
return filepath.Join(local, appDirName), nil
|
|
}
|
|
base, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolving user config dir: %w", err)
|
|
}
|
|
return filepath.Join(base, appDirName), nil
|
|
default:
|
|
if xdgState := os.Getenv("XDG_STATE_HOME"); xdgState != "" {
|
|
return filepath.Join(xdgState, "quoteforge"), nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolving user home dir: %w", err)
|
|
}
|
|
return filepath.Join(home, ".local", "state", "quoteforge"), nil
|
|
}
|
|
}
|
|
|
|
func exists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func copyIfExists(src, dst string) error {
|
|
if !exists(src) {
|
|
return nil
|
|
}
|
|
return copyFile(src, dst)
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
info, err := in.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode().Perm())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
if _, err := io.Copy(out, in); err != nil {
|
|
return err
|
|
}
|
|
|
|
return out.Sync()
|
|
}
|