// Package persistence provides repository implementations for the infrastructure layer. package persistence import ( "context" "os" "path/filepath" "sync" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/domain/errors" "github.com/router-for-me/CLIProxyAPI/v6/internal/domain/ports" ) // ConfigRepository implements the ports.ConfigRepository interface // using file system storage. type ConfigRepository struct { configPath string mu sync.RWMutex } // NewConfigRepository creates a new ConfigRepository func NewConfigRepository(configPath string) *ConfigRepository { return &ConfigRepository{ configPath: configPath, } } // Load retrieves the current configuration from file func (r *ConfigRepository) Load(ctx context.Context) (*config.Config, error) { r.mu.RLock() defer r.mu.RUnlock() if r.configPath == "" { return nil, errors.ErrConfigNotFound } cfg, err := config.LoadConfig(r.configPath) if err != nil { if os.IsNotExist(err) { return nil, errors.Wrap(errors.NotFound, "configuration file not found", err) } return nil, errors.Wrap(errors.InternalError, "failed to load configuration", err) } return cfg, nil } // Save persists the configuration to file func (r *ConfigRepository) Save(ctx context.Context, cfg *config.Config) error { return r.SaveWithPath(ctx, r.configPath, cfg) } // SaveWithPath persists the configuration to a specific path func (r *ConfigRepository) SaveWithPath(ctx context.Context, path string, cfg *config.Config) error { r.mu.Lock() defer r.mu.Unlock() if path == "" { return errors.New(errors.InvalidInput, "config path is empty") } if cfg == nil { return errors.New(errors.InvalidInput, "config is nil") } // Validate before saving if err := r.Validate(ctx, cfg); err != nil { return err } // Ensure directory exists dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return errors.Wrap(errors.InternalError, "failed to create config directory", err) } // Save with comment preservation if err := config.SaveConfigPreserveComments(path, cfg); err != nil { return errors.Wrap(errors.InternalError, "failed to save configuration", err) } return nil } // Validate validates the configuration without saving func (r *ConfigRepository) Validate(ctx context.Context, cfg *config.Config) error { if cfg == nil { return errors.ErrInvalidConfig.WithDetail("reason", "configuration is nil") } // Create temporary file for validation tmpDir := filepath.Dir(r.configPath) if tmpDir == "" { tmpDir = os.TempDir() } tmpFile, err := os.CreateTemp(tmpDir, "config-validate-*.yaml") if err != nil { return errors.Wrap(errors.InternalError, "failed to create temp file for validation", err) } tmpPath := tmpFile.Name() // Cleanup _ = tmpFile.Close() defer os.Remove(tmpPath) // Validate by loading _, err = config.LoadConfigOptional(tmpPath, false) if err != nil { return errors.Wrap(errors.ValidationFailed, "configuration validation failed", err) } return nil } // GetConfigPath returns the current configuration file path func (r *ConfigRepository) GetConfigPath() string { r.mu.RLock() defer r.mu.RUnlock() return r.configPath } // SetConfigPath updates the configuration file path func (r *ConfigRepository) SetConfigPath(path string) { r.mu.Lock() defer r.mu.Unlock() r.configPath = path } // Ensure ConfigRepository implements the interface var _ ports.ConfigRepository = (*ConfigRepository)(nil)