init
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package clicmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ConfigFile is the CLI's on-disk configuration.
|
||||
//
|
||||
// JSON rather than TOML: encoding/json is already linked into the binary for
|
||||
// the API, and adding a TOML parser to read four fields would cost more than
|
||||
// the file saves. CI never reads it at all — there, everything comes from
|
||||
// PAGES_* environment variables.
|
||||
type ConfigFile struct {
|
||||
Server string `json:"server,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Project string `json:"project,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultConfigPath is $PAGES_CONFIG, else $XDG_CONFIG_HOME/pages/config.json,
|
||||
// else ~/.config/pages/config.json. It returns "" when no home directory can be
|
||||
// determined, which simply means there is no config file.
|
||||
func DefaultConfigPath() string {
|
||||
if p := os.Getenv("PAGES_CONFIG"); p != "" {
|
||||
return p
|
||||
}
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(dir, "pages", "config.json")
|
||||
}
|
||||
|
||||
// LoadConfig reads path. A missing file is not an error — it is the normal
|
||||
// state on a CI runner. warn receives a note if the file is readable by anyone
|
||||
// but its owner, because it may hold a token.
|
||||
func LoadConfig(path string, warn io.Writer) (ConfigFile, error) {
|
||||
var f ConfigFile
|
||||
if path == "" {
|
||||
return f, nil
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return f, nil
|
||||
}
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("read config %s: %w", path, err)
|
||||
}
|
||||
if fi, err := os.Stat(path); err == nil && fi.Mode().Perm()&0o077 != 0 && warn != nil {
|
||||
fmt.Fprintf(warn, "warning: %s is mode %#o and may contain a token; run: chmod 600 %s\n",
|
||||
path, fi.Mode().Perm(), path)
|
||||
}
|
||||
if err := json.Unmarshal(raw, &f); err != nil {
|
||||
return ConfigFile{}, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// SaveConfig writes f to path with mode 0600, replacing any existing file
|
||||
// atomically so a crash cannot leave a truncated config behind.
|
||||
func SaveConfig(path string, f ConfigFile) error {
|
||||
if path == "" {
|
||||
return errors.New("no config path: set PAGES_CONFIG or pass --config")
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return fmt.Errorf("create config directory: %w", err)
|
||||
}
|
||||
raw, err := json.MarshalIndent(f, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
|
||||
// Written in the destination directory so the rename stays within one
|
||||
// filesystem, and created 0600 from the start so the token is never briefly
|
||||
// world-readable.
|
||||
tmp, err := os.CreateTemp(dir, ".config-*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary config: %w", err)
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if err := tmp.Chmod(0o600); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := tmp.Write(raw); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp.Name(), path); err != nil {
|
||||
return fmt.Errorf("install config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// redactToken shows enough of a token to recognise which one it is and nothing
|
||||
// that could be used with it. The key id is the public half by construction
|
||||
// (pgs_<keyid>_<secret>), so it is safe to print in full.
|
||||
func redactToken(token string) string {
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.SplitN(token, "_", 3)
|
||||
if len(parts) == 3 {
|
||||
return parts[0] + "_" + parts[1] + "_…"
|
||||
}
|
||||
return "…"
|
||||
}
|
||||
Reference in New Issue
Block a user