198 lines
4.4 KiB
Go
198 lines
4.4 KiB
Go
package appstate
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
const (
|
|
appDirName = "PriceForge"
|
|
defaultDB = "pfs.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) {
|
|
if explicitPath != "" {
|
|
return filepath.Clean(explicitPath), nil
|
|
}
|
|
|
|
if fromEnv := os.Getenv(envDBPath); fromEnv != "" {
|
|
return filepath.Clean(fromEnv), nil
|
|
}
|
|
|
|
dir, err := defaultStateDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(dir, defaultDB), nil
|
|
}
|
|
|
|
// ResolveConfigPath returns the config path using priority:
|
|
// explicit CLI path > QFS_CONFIG_PATH > OS-specific user state directory.
|
|
func ResolveConfigPath(explicitPath string) (string, error) {
|
|
if explicitPath != "" {
|
|
return filepath.Clean(explicitPath), nil
|
|
}
|
|
|
|
if fromEnv := os.Getenv(envCfgPath); fromEnv != "" {
|
|
return filepath.Clean(fromEnv), nil
|
|
}
|
|
|
|
dir, err := defaultStateDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(dir, defaultCfg), nil
|
|
}
|
|
|
|
// 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, "priceforge"), nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolving user home dir: %w", err)
|
|
}
|
|
return filepath.Join(home, ".local", "state", "priceforge"), 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()
|
|
}
|