feat: migrate config beside executable

This commit is contained in:
QiuSW
2026-07-27 11:33:22 +08:00
parent 206794f78c
commit eeb7950806
9 changed files with 307 additions and 42 deletions
+94 -1
View File
@@ -15,6 +15,8 @@ import (
const currentVersion = 1
var ErrDefaultConfigUnavailable = errors.New("default config is unavailable")
type Settings struct {
ChromePath string `json:"chromePath"`
EdgePath string `json:"edgePath"`
@@ -59,15 +61,106 @@ func New(path string) (*Store, error) {
}
func DefaultPath() (string, error) {
executable, err := os.Executable()
if err != nil {
return "", fmt.Errorf("%w: resolve executable", ErrDefaultConfigUnavailable)
}
return defaultPathForExecutable(executable)
}
// LegacyPath is the pre-portable configuration location. It is read only when
// the executable-side config does not exist yet.
func LegacyPath() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("resolve user config dir: %w", err)
return "", fmt.Errorf("%w: resolve legacy config directory", ErrDefaultConfigUnavailable)
}
return filepath.Join(dir, "chub", "config.json"), nil
}
// OpenDefault returns the config store beside the currently executing binary.
// A valid legacy user config is copied there once only when the target does not
// already exist. The legacy file is deliberately retained for recovery.
func OpenDefault() (*Store, error) {
target, err := DefaultPath()
if err != nil {
return nil, err
}
store, targetExists, err := defaultStore(target)
if err != nil || targetExists {
return store, err
}
legacy, err := LegacyPath()
if err != nil {
return nil, err
}
return migrateLegacyConfig(store, legacy)
}
func (s *Store) Path() string { return s.path }
func defaultPathForExecutable(executable string) (string, error) {
path := strings.TrimSpace(executable)
if path == "" || !filepath.IsAbs(path) {
return "", fmt.Errorf("%w: executable path must be absolute", ErrDefaultConfigUnavailable)
}
return filepath.Join(filepath.Dir(filepath.Clean(path)), "config.json"), nil
}
func openDefaultAt(target, legacy string) (*Store, error) {
store, targetExists, err := defaultStore(target)
if err != nil || targetExists {
return store, err
}
return migrateLegacyConfig(store, legacy)
}
func defaultStore(target string) (*Store, bool, error) {
store, err := New(target)
if err != nil {
return nil, false, fmt.Errorf("%w: initialize target store", ErrDefaultConfigUnavailable)
}
info, err := os.Stat(store.Path())
if errors.Is(err, os.ErrNotExist) {
return store, false, nil
}
if err != nil {
return nil, false, fmt.Errorf("%w: inspect target config", ErrDefaultConfigUnavailable)
}
if info.IsDir() {
return nil, false, fmt.Errorf("%w: target config is a directory", ErrDefaultConfigUnavailable)
}
return store, true, nil
}
func migrateLegacyConfig(target *Store, legacyPath string) (*Store, error) {
if target == nil {
return nil, fmt.Errorf("%w: target store is unavailable", ErrDefaultConfigUnavailable)
}
legacy, err := New(legacyPath)
if err != nil {
return nil, fmt.Errorf("%w: initialize legacy store", ErrDefaultConfigUnavailable)
}
if filepath.Clean(target.Path()) == filepath.Clean(legacy.Path()) {
return target, nil
}
info, err := os.Stat(legacy.Path())
if errors.Is(err, os.ErrNotExist) {
return target, nil
}
if err != nil || info.IsDir() {
return nil, fmt.Errorf("%w: inspect legacy config", ErrDefaultConfigUnavailable)
}
value, err := legacy.Load()
if err != nil {
return nil, fmt.Errorf("%w: validate legacy config", ErrDefaultConfigUnavailable)
}
if err := target.Save(value); err != nil {
return nil, fmt.Errorf("%w: migrate legacy config", ErrDefaultConfigUnavailable)
}
return target, nil
}
func (s *Store) Load() (File, error) {
data, err := os.ReadFile(s.path)
if errors.Is(err, os.ErrNotExist) {