package profile import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" ) type FileStore struct { path string } func NewFileStore(path string) *FileStore { return &FileStore{path: path} } func ConfigPath() (string, error) { configDir, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("locate user configuration directory: %w", err) } return filepath.Join(configDir, "proxyctl", "profiles.json"), nil } func (s *FileStore) Load() (State, error) { data, err := os.ReadFile(s.path) if errors.Is(err, fs.ErrNotExist) { return State{}, nil } if err != nil { return State{}, fmt.Errorf("read Profile configuration: %w", err) } var state State if err := json.Unmarshal(data, &state); err != nil { return State{}, fmt.Errorf("parse Profile configuration: %w", err) } return state, nil } func (s *FileStore) Save(state State) error { if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil { return fmt.Errorf("create Profile configuration directory: %w", err) } if err := os.Chmod(filepath.Dir(s.path), 0o700); err != nil { return fmt.Errorf("secure Profile configuration directory: %w", err) } data, err := json.MarshalIndent(state, "", " ") if err != nil { return fmt.Errorf("encode Profile configuration: %w", err) } data = append(data, '\n') temp, err := os.CreateTemp(filepath.Dir(s.path), ".profiles-*") if err != nil { return fmt.Errorf("create temporary Profile configuration: %w", err) } tempName := temp.Name() defer os.Remove(tempName) if err := temp.Chmod(0o600); err != nil { temp.Close() return fmt.Errorf("secure temporary Profile configuration: %w", err) } if _, err := temp.Write(data); err != nil { temp.Close() return fmt.Errorf("write temporary Profile configuration: %w", err) } if err := temp.Sync(); err != nil { temp.Close() return fmt.Errorf("sync temporary Profile configuration: %w", err) } if err := temp.Close(); err != nil { return fmt.Errorf("close temporary Profile configuration: %w", err) } if err := os.Rename(tempName, s.path); err != nil { return fmt.Errorf("replace Profile configuration: %w", err) } if err := os.Chmod(s.path, 0o600); err != nil { return fmt.Errorf("secure Profile configuration: %w", err) } dir, err := os.Open(filepath.Dir(s.path)) if err == nil { err = dir.Sync() _ = dir.Close() } if err != nil { return fmt.Errorf("sync Profile configuration directory: %w", err) } return nil }