package config import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "time" ) const DefaultUserAgent = "pan.baidu.com" type Config struct { ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` RedirectURI string `json:"redirect_uri"` AccessToken string `json:"access_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresAt time.Time `json:"expires_at,omitempty"` Root string `json:"root"` UserAgent string `json:"user_agent"` UploadParts int `json:"upload_parts"` PartSize int64 `json:"part_size,omitempty"` } func DefaultPath() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("find config directory: %w", err) } return filepath.Join(dir, "bdrclone", "config.json"), nil } func Load(path string) (*Config, error) { b, err := os.ReadFile(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("config %q does not exist; run `bdrclone config` first", path) } return nil, fmt.Errorf("read config: %w", err) } var cfg Config if err := json.Unmarshal(b, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } cfg.applyDefaults() return &cfg, cfg.Validate(false) } func Save(path string, cfg *Config) error { cfg.applyDefaults() if err := cfg.Validate(false); err != nil { return err } b, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("encode config: %w", err) } b = append(b, '\n') if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return fmt.Errorf("create config directory: %w", err) } tmp, err := os.CreateTemp(filepath.Dir(path), ".config-*") if err != nil { return fmt.Errorf("create temporary config: %w", err) } tmpName := tmp.Name() defer os.Remove(tmpName) if err := tmp.Chmod(0o600); err != nil { tmp.Close() return fmt.Errorf("secure config: %w", err) } if _, err := tmp.Write(b); err != nil { tmp.Close() return fmt.Errorf("write config: %w", err) } if err := tmp.Close(); err != nil { return fmt.Errorf("close config: %w", err) } if err := os.Rename(tmpName, path); err != nil { return fmt.Errorf("replace config: %w", err) } return nil } func (c *Config) Validate(requireToken bool) error { if strings.TrimSpace(c.ClientID) == "" || strings.TrimSpace(c.ClientSecret) == "" { return errors.New("client_id and client_secret are required") } if requireToken && c.RefreshToken == "" && c.AccessToken == "" { return errors.New("no OAuth token; run `bdrclone auth` first") } if c.UploadParts < 1 || c.UploadParts > 32 { return errors.New("upload_parts must be between 1 and 32") } if c.PartSize != 0 && c.PartSize < 4<<20 { return errors.New("part_size must be 0 or at least 4 MiB") } if c.PartSize > 32<<20 { return errors.New("part_size cannot exceed Baidu's 32 MiB maximum") } return nil } func (c *Config) applyDefaults() { if c.RedirectURI == "" { c.RedirectURI = "http://127.0.0.1:53682/callback" } if c.Root == "" { c.Root = "/" } if !strings.HasPrefix(c.Root, "/") { c.Root = "/" + c.Root } c.Root = strings.TrimSuffix(c.Root, "/") if c.Root == "" { c.Root = "/" } if c.UserAgent == "" { c.UserAgent = DefaultUserAgent } if c.UploadParts == 0 { c.UploadParts = 3 } }