Files
cursor-byok/internal/client/config_transfer.go
T

196 lines
5.7 KiB
Go

package client
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
serverconfig "cursor/internal/backend/server/config"
"gopkg.in/yaml.v3"
)
const maxConfigTransferFileSize = 4 << 20
type importedUserConfigDocument struct {
serverconfig.Config `yaml:",inline"`
LegacyRouting any `yaml:"routing,omitempty"`
}
// ExportUserConfig 将当前完整配置导出为 YAML 文件。
func (s *ProxyService) ExportUserConfig(path string) (string, error) {
if s == nil {
return "", errors.New("配置服务未初始化")
}
targetPath := normalizeConfigExportPath(path)
if targetPath == "" {
return "", errors.New("导出路径不能为空")
}
s.configMu.Lock()
defer s.configMu.Unlock()
cfg, err := s.loadUserConfig()
if err != nil {
return "", fmt.Errorf("读取当前配置失败: %w", err)
}
data, err := yaml.Marshal(cfg)
if err != nil {
return "", fmt.Errorf("序列化导出配置失败: %w", err)
}
if err := writeExportedUserConfig(targetPath, data); err != nil {
return "", err
}
return targetPath, nil
}
// ImportUserConfig 从 YAML 文件校验并替换当前完整配置。
func (s *ProxyService) ImportUserConfig(path string) (UserConfig, error) {
if s == nil {
return serverconfig.Config{}, errors.New("配置服务未初始化")
}
s.lifecycleMu.Lock()
defer s.lifecycleMu.Unlock()
s.configMu.Lock()
defer s.configMu.Unlock()
if err := ensureConfigImportAllowed(s.GetState()); err != nil {
return serverconfig.Config{}, err
}
data, err := readImportedUserConfig(path)
if err != nil {
return serverconfig.Config{}, err
}
cfg, err := decodeImportedUserConfig(data)
if err != nil {
return serverconfig.Config{}, err
}
if err := s.saveUserConfig(cfg); err != nil {
return serverconfig.Config{}, fmt.Errorf("保存导入配置失败: %w", err)
}
persisted, err := s.loadUserConfig()
if err != nil {
return serverconfig.Config{}, fmt.Errorf("重新读取导入配置失败: %w", err)
}
return persisted, nil
}
func normalizeConfigExportPath(path string) string {
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return ""
}
extension := strings.ToLower(filepath.Ext(trimmed))
if extension != ".yaml" && extension != ".yml" {
trimmed += ".yaml"
}
return filepath.Clean(trimmed)
}
func writeExportedUserConfig(path string, data []byte) error {
directory := filepath.Dir(path)
file, err := os.CreateTemp(directory, ".cursor-byok-config-*.tmp")
if err != nil {
return fmt.Errorf("创建导出配置临时文件失败: %w", err)
}
temporaryPath := file.Name()
defer os.Remove(temporaryPath)
if err := file.Chmod(0o600); err != nil {
_ = file.Close()
return fmt.Errorf("设置导出配置权限失败: %w", err)
}
if _, err := file.Write(data); err != nil {
_ = file.Close()
return fmt.Errorf("写入导出配置失败: %w", err)
}
if err := file.Sync(); err != nil {
_ = file.Close()
return fmt.Errorf("同步导出配置失败: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("关闭导出配置失败: %w", err)
}
if err := replaceExportFile(temporaryPath, path); err != nil {
return fmt.Errorf("替换导出配置失败: %w", err)
}
return nil
}
func ensureConfigImportAllowed(state ProxyState) error {
if state.BackendRunning || state.ProxyRunning || state.Running {
return errors.New("服务运行中不能导入完整配置,请先停止服务")
}
return nil
}
func readImportedUserConfig(path string) ([]byte, error) {
sourcePath := strings.TrimSpace(path)
if sourcePath == "" {
return nil, errors.New("导入路径不能为空")
}
file, err := os.Open(sourcePath)
if err != nil {
return nil, fmt.Errorf("打开导入配置失败: %w", err)
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("读取导入配置信息失败: %w", err)
}
if !info.Mode().IsRegular() {
return nil, errors.New("导入配置必须是普通文件")
}
if info.Size() > maxConfigTransferFileSize {
return nil, fmt.Errorf("导入配置不能超过 %d MiB", maxConfigTransferFileSize>>20)
}
data, err := io.ReadAll(io.LimitReader(file, maxConfigTransferFileSize+1))
if err != nil {
return nil, fmt.Errorf("读取导入配置失败: %w", err)
}
if len(data) > maxConfigTransferFileSize {
return nil, fmt.Errorf("导入配置不能超过 %d MiB", maxConfigTransferFileSize>>20)
}
return data, nil
}
func decodeImportedUserConfig(data []byte) (serverconfig.Config, error) {
if err := validateImportedUserConfigDocument(data); err != nil {
return serverconfig.Config{}, err
}
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
var document importedUserConfigDocument
if err := decoder.Decode(&document); err != nil {
return serverconfig.Config{}, fmt.Errorf("导入配置包含未知字段或无效 YAML: %w", err)
}
var trailing any
if err := decoder.Decode(&trailing); err == nil {
return serverconfig.Config{}, errors.New("导入配置只能包含单个 YAML 文档")
} else if !errors.Is(err, io.EOF) {
return serverconfig.Config{}, fmt.Errorf("解析导入配置尾部失败: %w", err)
}
normalized, err := serverconfig.NormalizeConfig(document.Config)
if err != nil {
return serverconfig.Config{}, fmt.Errorf("导入配置校验失败: %w", err)
}
return normalized, nil
}
func validateImportedUserConfigDocument(data []byte) error {
decoder := yaml.NewDecoder(bytes.NewReader(data))
var document yaml.Node
if err := decoder.Decode(&document); err != nil {
if errors.Is(err, io.EOF) {
return errors.New("导入配置不能为空")
}
return fmt.Errorf("导入配置不是有效 YAML: %w", err)
}
if len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode {
return errors.New("导入配置顶层必须是 YAML 对象")
}
return nil
}