Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
744ae45c15 |
101
utils/envx/envx.go
Normal file
101
utils/envx/envx.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package envx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get returns the environment variable or the provided default if unset.
|
||||||
|
func Get(key, def string) string {
|
||||||
|
if v, ok := os.LookupEnv(key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGet returns the environment variable or panics if it is not set.
|
||||||
|
func MustGet(key string) string {
|
||||||
|
if v, ok := os.LookupEnv(key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
panic("required env var not set: " + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool parses a boolean-like env var with a default fallback.
|
||||||
|
// Recognized true values: 1, t, true, y, yes, on (case-insensitive)
|
||||||
|
// Recognized false values: 0, f, false, n, no, off
|
||||||
|
func Bool(key string, def bool) bool {
|
||||||
|
v, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
s := strings.TrimSpace(strings.ToLower(v))
|
||||||
|
switch s {
|
||||||
|
case "1", "t", "true", "y", "yes", "on":
|
||||||
|
return true
|
||||||
|
case "0", "f", "false", "n", "no", "off":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int parses an int env var with a default fallback.
|
||||||
|
func Int(key string, def int) int {
|
||||||
|
v, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(strings.TrimSpace(v))
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 parses an int64 env var with a default fallback.
|
||||||
|
func Int64(key string, def int64) int64 {
|
||||||
|
v, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 parses a float64 env var with a default fallback.
|
||||||
|
func Float64(key string, def float64) float64 {
|
||||||
|
v, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseFloat(strings.TrimSpace(v), 64)
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration parses a time.Duration env var with a default fallback.
|
||||||
|
func Duration(key string, def time.Duration) time.Duration {
|
||||||
|
v, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
d, err := time.ParseDuration(strings.TrimSpace(v))
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand expands ${var} or $var in the string according to the values of the current environment.
|
||||||
|
func Expand(s string) string {
|
||||||
|
return os.ExpandEnv(s)
|
||||||
|
}
|
||||||
|
|
||||||
209
utils/filex/filex.go
Normal file
209
utils/filex/filex.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package filex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exists reports whether the given path exists (file or directory).
|
||||||
|
func Exists(path string) bool {
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFile reports whether the path exists and is a regular file.
|
||||||
|
func IsFile(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.Mode().IsRegular()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir reports whether the path exists and is a directory.
|
||||||
|
func IsDir(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReadable reports whether the target can be opened for reading.
|
||||||
|
func IsReadable(path string) bool {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWritable reports whether a file can be written to. If the file does not exist,
|
||||||
|
// it checks writability of the parent directory by attempting to create a temp file.
|
||||||
|
func IsWritable(path string) bool {
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
// Try writing a temp file in the directory
|
||||||
|
f, err := os.CreateTemp(path, ".wtmp-*")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name := f.Name()
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Try opening for write
|
||||||
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Not exist: check parent dir is writable by creating a temp file
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if dir == "." || dir == "" {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(dir, ".wtmp-*")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name := f.Name()
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExecutable reports whether a file is considered executable on the current OS.
|
||||||
|
// On Unix, checks the executable bits; on Windows, checks the extension against PATHEXT.
|
||||||
|
func IsExecutable(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
if ext == ".exe" || ext == ".bat" || ext == ".cmd" || ext == ".com" || ext == ".ps1" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
pathext := strings.ToLower(os.Getenv("PATHEXT"))
|
||||||
|
for _, e := range strings.Split(pathext, ";") {
|
||||||
|
if e != "" && ext == strings.ToLower(e) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Unix permissions: any execute bit set
|
||||||
|
return info.Mode()&0o111 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the file at path.
|
||||||
|
func Size(path string) (int64, error) {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return info.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch creates the file if it does not exist, or updates its modification time if it does.
|
||||||
|
func Touch(path string) error {
|
||||||
|
now := time.Now()
|
||||||
|
if Exists(path) {
|
||||||
|
return os.Chtimes(path, now, now)
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDir ensures that a directory exists with the specified permission bits.
|
||||||
|
func EnsureDir(path string, perm fs.FileMode) error {
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if Exists(path) {
|
||||||
|
if !IsDir(path) {
|
||||||
|
return errors.New("path exists and is not a directory")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.MkdirAll(path, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindInPath searches for an executable named name in the system PATH.
|
||||||
|
func FindInPath(name string) (string, bool) {
|
||||||
|
p, err := exec.LookPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return p, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFileString reads a whole file into a string.
|
||||||
|
func ReadFileString(path string) (string, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFileString writes a string to a file with the given permissions, creating or truncating it.
|
||||||
|
func WriteFileString(path, s string, perm fs.FileMode) error {
|
||||||
|
return os.WriteFile(path, []byte(s), perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFileAtomic writes data to a temporary file in the same directory and renames it into place.
|
||||||
|
func WriteFileAtomic(path string, data []byte, perm fs.FileMode) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := EnsureDir(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(dir, ".atomic-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmp := f.Name()
|
||||||
|
// Best-effort cleanup on error
|
||||||
|
defer func() { _ = os.Remove(tmp) }()
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.Chmod(perm); err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.Sync(); err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmp, path)
|
||||||
|
}
|
||||||
|
|
||||||
28
utils/jsonx/json.go
Normal file
28
utils/jsonx/json.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package jsonx
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
func Marshal(v any) ([]byte, error) {
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal[T any](data []byte) (*T, error) {
|
||||||
|
var v T
|
||||||
|
err := json.Unmarshal(data, &v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalTo(data []byte, a any) error {
|
||||||
|
return json.Unmarshal(data, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustUnmarshal[T any](data []byte) *T {
|
||||||
|
v, err := Unmarshal[T](data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
16
utils/ptr/ptr.go
Normal file
16
utils/ptr/ptr.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package ptr
|
||||||
|
|
||||||
|
// Of returns a pointer to v.
|
||||||
|
func Of[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
// Deref returns the value pointed to by p, or def if p is nil.
|
||||||
|
func Deref[T any](p *T, def T) T {
|
||||||
|
if p == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return *p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero returns the zero value for type T.
|
||||||
|
func Zero[T any]() (z T) { return }
|
||||||
|
|
||||||
77
utils/randx/randx.go
Normal file
77
utils/randx/randx.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package randx
|
||||||
|
|
||||||
|
import (
|
||||||
|
crand "crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bytes returns n cryptographically secure random bytes.
|
||||||
|
func Bytes(n int) ([]byte, error) {
|
||||||
|
if n <= 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := io.ReadFull(crand.Reader, b)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var urlSafe = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
|
||||||
|
|
||||||
|
// String returns a URL-safe random string of length n using a cryptographically secure source.
|
||||||
|
func String(n int) (string, error) {
|
||||||
|
if n <= 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
out := make([]byte, n)
|
||||||
|
// rejection sampling to avoid modulo bias
|
||||||
|
// we want uniform selection from urlSafe (len m)
|
||||||
|
m := len(urlSafe)
|
||||||
|
threshold := 256 - (256 % m)
|
||||||
|
var buf [1]byte
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
for {
|
||||||
|
if _, err := io.ReadFull(crand.Reader, buf[:]); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b := int(buf[0])
|
||||||
|
if b < threshold {
|
||||||
|
out[i] = urlSafe[b%m]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustString is like String but panics on error.
|
||||||
|
func MustString(n int) string {
|
||||||
|
s, err := String(n)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUIDv4 generates a RFC 4122 version 4 UUID string.
|
||||||
|
func UUIDv4() (string, error) {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := io.ReadFull(crand.Reader, b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Set version (4) and variant (10)
|
||||||
|
b[6] = (b[6] & 0x0f) | 0x40
|
||||||
|
b[8] = (b[8] & 0x3f) | 0x80
|
||||||
|
// Format 8-4-4-4-12
|
||||||
|
dst := make([]byte, 36)
|
||||||
|
hex.Encode(dst[0:8], b[0:4])
|
||||||
|
dst[8] = '-'
|
||||||
|
hex.Encode(dst[9:13], b[4:6])
|
||||||
|
dst[13] = '-'
|
||||||
|
hex.Encode(dst[14:18], b[6:8])
|
||||||
|
dst[18] = '-'
|
||||||
|
hex.Encode(dst[19:23], b[8:10])
|
||||||
|
dst[23] = '-'
|
||||||
|
hex.Encode(dst[24:36], b[10:16])
|
||||||
|
return string(dst), nil
|
||||||
|
}
|
||||||
60
utils/strx/strx.go
Normal file
60
utils/strx/strx.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package strx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Coalesce returns the first non-empty string from vals, or "" if all are empty.
|
||||||
|
func Coalesce(vals ...string) string {
|
||||||
|
for _, v := range vals {
|
||||||
|
if v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsFold reports whether substr is within s, case-insensitively.
|
||||||
|
func ContainsFold(s, substr string) bool {
|
||||||
|
if substr == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitAndTrim splits s by sep, trims spaces for each part, and filters empty items.
|
||||||
|
func SplitAndTrim(s, sep string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(s, sep)
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p != "" {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinNonEmpty joins parts using sep, skipping empty strings.
|
||||||
|
func JoinNonEmpty(sep string, parts ...string) string {
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
if p != "" {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(out, sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimAll collapses any whitespace runs to single spaces and trims both ends.
|
||||||
|
func TrimAll(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
return strings.Join(fields, " ")
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user