Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
142 lines
3.5 KiB
Go
142 lines
3.5 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
|
|
// Package tomlfile writes TOML files atomically with owner-only
|
|
// permissions, shared by the users, tokens and template stores.
|
|
package tomlfile
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"sourcedock.dev/petrbalvin/interpres/v2"
|
|
)
|
|
|
|
// Write persists rows as the array of tables name, and replaces path
|
|
// atomically at mode 0600.
|
|
//
|
|
// The rows are []map[string]any rather than []any on purpose: interpres
|
|
// renders an array of tables as one [[name]] block per row, which is the
|
|
// form these files are documented in and the form a reader can edit, while
|
|
// a []any of tables renders as a single inline array.
|
|
func Write(path, name string, rows []map[string]any) error {
|
|
payload, err := interpres.Marshal(map[string]any{name: rows})
|
|
if err != nil {
|
|
return fmt.Errorf("encode toml: %w", err)
|
|
}
|
|
parent := filepath.Dir(path)
|
|
if parent == "" {
|
|
parent = "."
|
|
}
|
|
if err := os.MkdirAll(parent, 0o755); err != nil {
|
|
return fmt.Errorf("create directory: %w", err)
|
|
}
|
|
tmp, err := os.CreateTemp(parent, ".toml-*.tmp")
|
|
if err != nil {
|
|
return fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
tmpPath := tmp.Name()
|
|
if _, err := tmp.Write(payload); err != nil {
|
|
tmp.Close()
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("write: %w", err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("sync: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("close: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, path); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("replace: %w", err)
|
|
}
|
|
if err := os.Chmod(path, 0o600); err != nil {
|
|
return fmt.Errorf("chmod: %w", err)
|
|
}
|
|
syncDir(parent)
|
|
return nil
|
|
}
|
|
|
|
func syncDir(dir string) {
|
|
d, err := os.Open(dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer d.Close()
|
|
_ = d.Sync()
|
|
}
|
|
|
|
// Tables normalises a TOML array of tables to a slice of maps.
|
|
// interpres decodes [[section]] arrays as []map[string]any, while
|
|
// inline arrays decode to []any; both are accepted.
|
|
func Tables(value any) []map[string]any {
|
|
switch list := value.(type) {
|
|
case []map[string]any:
|
|
return list
|
|
case []any:
|
|
out := make([]map[string]any, 0, len(list))
|
|
for _, item := range list {
|
|
if table, ok := item.(map[string]any); ok {
|
|
out = append(out, table)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Strings normalises a TOML array of strings.
|
|
func Strings(value any) []string {
|
|
var list []any
|
|
switch v := value.(type) {
|
|
case []string:
|
|
return v
|
|
case []any:
|
|
list = v
|
|
default:
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(list))
|
|
for _, item := range list {
|
|
if s, ok := item.(string); ok {
|
|
out = append(out, s)
|
|
} else {
|
|
out = append(out, "")
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// String returns the value of a decoded TOML string field, and "" for a
|
|
// missing or non-string value. It is the shared accessor for the files
|
|
// that read hand-edited TOML.
|
|
func String(value any) string {
|
|
s, _ := value.(string)
|
|
return s
|
|
}
|
|
|
|
// Bool returns the value of a decoded TOML boolean field, and fallback
|
|
// for a missing or non-boolean value, so an omitted key can mean the
|
|
// value the reader wants.
|
|
func Bool(value any, fallback bool) bool {
|
|
b, ok := value.(bool)
|
|
if !ok {
|
|
return fallback
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Int64 returns the value of a decoded TOML integer field, and fallback
|
|
// for a missing or non-integer value.
|
|
func Int64(value any, fallback int64) int64 {
|
|
if i, ok := value.(int64); ok {
|
|
return i
|
|
}
|
|
return fallback
|
|
}
|