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() }