Files
Cyrene_v2/internal/user/save.go
T
Aska 5a7ee9d772
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(owner): 扫码绑定确认时自动把主人id登记进owner.yaml白名单(热生效)
2026-09-08 02:53:01 +08:00

92 lines
3.0 KiB
Go

package user
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// SaveOwnerProfile writes p to path as YAML, atomically (temp file + rename) with
// mode 0600 and lenient parent-directory creation, matching the runtime config
// convention (SaveChannels). The whole struct is marshaled, so curated fields
// (notes, identityFile, displayName, ...) survive a round-trip. YAML comments in
// the existing file are not part of the struct and are dropped by the encode,
// consistent with the project's other config writers.
func SaveOwnerProfile(path string, p *OwnerProfile) error {
if p == nil {
return fmt.Errorf("user: save owner profile: empty profile")
}
if strings.TrimSpace(path) == "" {
return fmt.Errorf("user: save owner profile: empty path")
}
data, err := yaml.Marshal(p)
if err != nil {
return fmt.Errorf("user: encode %s: %w", path, err)
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("user: create %s: %w", dir, err)
}
tmp, err := os.CreateTemp(dir, ".owner-*.yaml")
if err != nil {
return fmt.Errorf("user: temp file in %s: %w", dir, err)
}
tmpName := tmp.Name()
closed := false
defer func() {
_ = tmp.Close()
if !closed {
_ = os.Remove(tmpName)
}
}()
// CreateTemp opens with 0600; chmod explicitly so the on-disk file never
// carries a broader mode even if the umask changes.
if err := tmp.Chmod(0o600); err != nil {
return fmt.Errorf("user: chmod %s: %w", tmpName, err)
}
if _, err := tmp.Write(data); err != nil {
return fmt.Errorf("user: write %s: %w", tmpName, err)
}
if err := tmp.Sync(); err != nil {
return fmt.Errorf("user: sync %s: %w", tmpName, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("user: close %s: %w", tmpName, err)
}
closed = true
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("user: rename to %s: %w", path, err)
}
// Best-effort dir fsync so the rename is durable across a crash.
if d, derr := os.Open(dir); derr == nil {
_ = d.Sync()
_ = d.Close()
}
return nil
}
// RegisterOwnerAccount loads the owner whitelist at path, appends (platform,
// accountID) when it is not already present (idempotent), and writes the updated
// profile back. It returns an error when the profile cannot be loaded or saved.
// The on-disk replace is atomic, so a concurrent reader never sees a partial
// file. Hot-reload of a running owner profile is the caller's concern (see
// cmd/cyrene's ownerRegistrar), which applies RegisterAccount to the live profile
// the channel bridge already holds.
func RegisterOwnerAccount(path, platform, accountID string) error {
if strings.TrimSpace(platform) == "" {
return fmt.Errorf("user: register owner account: empty platform")
}
if strings.TrimSpace(accountID) == "" {
return fmt.Errorf("user: register owner account: empty account id")
}
p, err := LoadOwnerProfile(path)
if err != nil {
return fmt.Errorf("user: load %s: %w", path, err)
}
p.RegisterAccount(platform, accountID)
return SaveOwnerProfile(path, p)
}