mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-01 04:20:53 +09:00
d20a412079
UnmarshalByExt picks JSON, HuJSON or YAML from the file name; content can't tell them apart, since YAML parses JSON syntax.
115 lines
2.8 KiB
Go
115 lines
2.8 KiB
Go
package util
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
"github.com/tailscale/hujson"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
Base8 = 8
|
|
Base10 = 10
|
|
BitSize16 = 16
|
|
BitSize32 = 32
|
|
BitSize64 = 64
|
|
PermissionFallback = 0o700
|
|
)
|
|
|
|
// ErrDirectoryPermission is returned when creating a directory fails due to permission issues.
|
|
var ErrDirectoryPermission = errors.New("creating directory failed with permission error")
|
|
|
|
// ErrUnknownFileFormat is returned for a file whose extension names no format
|
|
// [UnmarshalByExt] reads.
|
|
var ErrUnknownFileFormat = errors.New("unknown file format, want .json, .hujson, .yaml or .yml")
|
|
|
|
// UnmarshalByExt decodes data into a T in the format name's extension picks:
|
|
// .json, .hujson (JSON with comments and trailing commas), or .yaml/.yml.
|
|
// The extension decides, not the content: YAML parses JSON syntax, so sniffing
|
|
// cannot tell the two apart. YAML keys are the lowercased Go field names.
|
|
func UnmarshalByExt[T any](name string, data []byte) (T, error) {
|
|
var (
|
|
v T
|
|
err error
|
|
)
|
|
|
|
switch ext := strings.ToLower(filepath.Ext(name)); ext {
|
|
case ".json":
|
|
err = json.Unmarshal(data, &v)
|
|
case ".hujson":
|
|
data, err = hujson.Standardize(data)
|
|
if err == nil {
|
|
err = json.Unmarshal(data, &v)
|
|
}
|
|
case ".yaml", ".yml":
|
|
err = yaml.Unmarshal(data, &v)
|
|
default:
|
|
return v, fmt.Errorf("%s: %w", name, ErrUnknownFileFormat)
|
|
}
|
|
|
|
if err != nil {
|
|
return v, fmt.Errorf("decoding %s: %w", name, err)
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// ReadFileByExt reads the file at path and decodes it with [UnmarshalByExt].
|
|
func ReadFileByExt[T any](path string) (T, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
var zero T
|
|
|
|
return zero, err
|
|
}
|
|
|
|
return UnmarshalByExt[T](path, data)
|
|
}
|
|
|
|
func AbsolutePathFromConfigPath(path string) string {
|
|
// If a relative path is provided, prefix it with the directory where
|
|
// the config file was found.
|
|
if (path != "") && !strings.HasPrefix(path, string(os.PathSeparator)) {
|
|
dir, _ := filepath.Split(viper.ConfigFileUsed())
|
|
if dir != "" {
|
|
path = filepath.Join(dir, path)
|
|
}
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
func GetFileMode(key string) fs.FileMode {
|
|
modeStr := viper.GetString(key)
|
|
|
|
mode, err := strconv.ParseUint(modeStr, Base8, BitSize64)
|
|
if err != nil {
|
|
return PermissionFallback
|
|
}
|
|
|
|
return fs.FileMode(mode) //nolint:gosec // file mode is bounded by ParseUint
|
|
}
|
|
|
|
func EnsureDir(dir string) error {
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) { //nolint:noinlineerr
|
|
err := os.MkdirAll(dir, PermissionFallback)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrPermission) {
|
|
return fmt.Errorf("%w: %s", ErrDirectoryPermission, dir)
|
|
}
|
|
|
|
return fmt.Errorf("creating directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|