This commit is contained in:
2025-09-16 14:27:34 +03:00
commit afeb139f5a
21 changed files with 4714 additions and 0 deletions

233
pkg/utils/helpers.go Normal file
View File

@@ -0,0 +1,233 @@
package utils
import (
"bufio"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
)
// FileExists checks if a file exists
func FileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
// ReadFileLines reads a file and returns its lines
func ReadFileLines(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, scanner.Err()
}
// ReadFirstLine reads the first line of a file
func ReadFirstLine(path string) (string, error) {
lines, err := ReadFileLines(path)
if err != nil {
return "", err
}
if len(lines) == 0 {
return "", fmt.Errorf("file is empty")
}
return lines[0], nil
}
// ParseKeyValue parses a key=value string
func ParseKeyValue(line string) (string, string, bool) {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
return "", "", false
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Remove quotes if present
value = strings.Trim(value, `"'`)
return key, value, true
}
// ParseInt64 safely parses a string to int64
func ParseInt64(s string) int64 {
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0
}
return val
}
// ParseInt safely parses a string to int
func ParseInt(s string) int {
val, err := strconv.Atoi(s)
if err != nil {
return 0
}
return val
}
// IsRoot checks if the current user is root
func IsRoot() bool {
return os.Geteuid() == 0
}
// RunCommand executes a command and returns its output
func RunCommand(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
// RunCommandWithInput executes a command with stdin input
func RunCommandWithInput(input, name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
cmd.Stdin = strings.NewReader(input)
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
// CommandExists checks if a command exists in PATH
func CommandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// GetUserConfirmation prompts user for yes/no confirmation
func GetUserConfirmation(prompt string) bool {
fmt.Printf("%s (y/N): ", prompt)
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
return response == "y" || response == "yes"
}
// GetUserInput prompts user for input with a default value
func GetUserInput(prompt, defaultValue string) string {
if defaultValue != "" {
fmt.Printf("%s [%s]: ", prompt, defaultValue)
} else {
fmt.Printf("%s: ", prompt)
}
var input string
fmt.Scanln(&input)
input = strings.TrimSpace(input)
if input == "" {
return defaultValue
}
return input
}
// EnsureRoot ensures the program is running with root privileges
func EnsureRoot() error {
if !IsRoot() {
return fmt.Errorf("this operation requires root privileges")
}
return nil
}
// RequestElevation requests privilege elevation using sudo
func RequestElevation(args []string) error {
if IsRoot() {
return nil
}
fmt.Println("This operation requires elevated privileges.")
if !GetUserConfirmation("Continue with sudo?") {
return fmt.Errorf("operation cancelled by user")
}
// Prepare sudo command
sudoArgs := append([]string{os.Args[0]}, args...)
cmd := exec.Command("sudo", sudoArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Execute with sudo
err := cmd.Run()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
os.Exit(status.ExitStatus())
}
}
return fmt.Errorf("failed to execute with elevated privileges: %w", err)
}
// Exit successfully since the elevated process completed
os.Exit(0)
return nil // This line will never be reached
}
// SanitizeInput sanitizes user input by removing potentially dangerous characters
func SanitizeInput(input string) string {
// Remove null bytes and control characters
sanitized := strings.ReplaceAll(input, "\x00", "")
sanitized = strings.TrimSpace(sanitized)
// Basic validation - reject inputs with shell metacharacters
dangerous := []string{";", "&", "|", "`", "$", "(", ")", "<", ">", "\"", "'"}
for _, char := range dangerous {
if strings.Contains(sanitized, char) {
return ""
}
}
return sanitized
}
// FormatBytes formats bytes into human-readable format
func FormatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
units := []string{"KB", "MB", "GB", "TB", "PB"}
return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp])
}
// Contains checks if a slice contains a string
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// RemoveEmpty removes empty strings from a slice
func RemoveEmpty(slice []string) []string {
var result []string
for _, s := range slice {
if strings.TrimSpace(s) != "" {
result = append(result, s)
}
}
return result
}

174
pkg/utils/helpers_test.go Normal file
View File

@@ -0,0 +1,174 @@
package utils
import (
"os"
"testing"
)
func TestFileExists(t *testing.T) {
tempFile, err := os.CreateTemp("", "test_file")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile.Name())
defer tempFile.Close()
if !FileExists(tempFile.Name()) {
t.Errorf("FileExists should return true for existing file")
}
if FileExists("/non/existent/file") {
t.Errorf("FileExists should return false for non-existing file")
}
}
func TestParseKeyValue(t *testing.T) {
tests := []struct {
input string
expectedKey string
expectedVal string
expectedOk bool
}{
{"key=value", "key", "value", true},
{"key = value", "key", "value", true},
{"key=\"quoted value\"", "key", "quoted value", true},
{"key='single quoted'", "key", "single quoted", true},
{"invalid_line", "", "", false},
{"key=", "key", "", true},
{"=value", "", "value", true},
}
for _, test := range tests {
key, value, ok := ParseKeyValue(test.input)
if key != test.expectedKey || value != test.expectedVal || ok != test.expectedOk {
t.Errorf("ParseKeyValue(%q) = (%q, %q, %v), want (%q, %q, %v)",
test.input, key, value, ok, test.expectedKey, test.expectedVal, test.expectedOk)
}
}
}
func TestParseInt64(t *testing.T) {
tests := []struct {
input string
expected int64
}{
{"123", 123},
{"0", 0},
{"-456", -456},
{"invalid", 0},
{"", 0},
{"123.45", 0}, // Should fail for float
}
for _, test := range tests {
result := ParseInt64(test.input)
if result != test.expected {
t.Errorf("ParseInt64(%q) = %d, want %d", test.input, result, test.expected)
}
}
}
func TestParseInt(t *testing.T) {
tests := []struct {
input string
expected int
}{
{"123", 123},
{"0", 0},
{"-456", -456},
{"invalid", 0},
{"", 0},
}
for _, test := range tests {
result := ParseInt(test.input)
if result != test.expected {
t.Errorf("ParseInt(%q) = %d, want %d", test.input, result, test.expected)
}
}
}
func TestSanitizeInput(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"clean_input", "clean_input"},
{" spaced ", "spaced"},
{"with;semicolon", ""},
{"with&ampersand", ""},
{"with|pipe", ""},
{"with`backtick", ""},
{"with$dollar", ""},
{"with(paren", ""},
{"with\"quote", ""},
{"with'quote", ""},
{"normal_text_123", "normal_text_123"},
}
for _, test := range tests {
result := SanitizeInput(test.input)
if result != test.expected {
t.Errorf("SanitizeInput(%q) = %q, want %q", test.input, result, test.expected)
}
}
}
func TestFormatBytes(t *testing.T) {
tests := []struct {
input int64
expected string
}{
{500, "500 B"},
{1024, "1.0 KB"},
{1536, "1.5 KB"},
{1048576, "1.0 MB"},
{1073741824, "1.0 GB"},
{0, "0 B"},
}
for _, test := range tests {
result := FormatBytes(test.input)
if result != test.expected {
t.Errorf("FormatBytes(%d) = %q, want %q", test.input, result, test.expected)
}
}
}
func TestContains(t *testing.T) {
slice := []string{"apple", "banana", "cherry"}
tests := []struct {
item string
expected bool
}{
{"apple", true},
{"banana", true},
{"cherry", true},
{"grape", false},
{"", false},
}
for _, test := range tests {
result := Contains(slice, test.item)
if result != test.expected {
t.Errorf("Contains(slice, %q) = %v, want %v", test.item, result, test.expected)
}
}
}
func TestRemoveEmpty(t *testing.T) {
input := []string{"apple", "", "banana", " ", "cherry", "\t\n"}
expected := []string{"apple", "banana", "cherry"}
result := RemoveEmpty(input)
if len(result) != len(expected) {
t.Fatalf("RemoveEmpty() returned slice of length %d, want %d", len(result), len(expected))
}
for i, item := range result {
if item != expected[i] {
t.Errorf("RemoveEmpty()[%d] = %q, want %q", i, item, expected[i])
}
}
}

83
pkg/utils/logger.go Normal file
View File

@@ -0,0 +1,83 @@
package utils
import (
"io"
"log/slog"
"os"
"time"
)
// Logger provides structured logging capabilities
type Logger struct {
*slog.Logger
}
// NewLogger creates a new structured logger
func NewLogger() *Logger {
return NewLoggerWithOutput(os.Stdout)
}
// NewDebugLogger creates a new logger with debug level enabled
func NewDebugLogger() *Logger {
opts := &slog.HandlerOptions{
Level: slog.LevelDebug,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Customize timestamp format
if a.Key == slog.TimeKey {
return slog.Attr{
Key: a.Key,
Value: slog.StringValue(time.Now().Format("2006-01-02 15:04:05")),
}
}
return a
},
}
handler := slog.NewTextHandler(os.Stdout, opts)
logger := slog.New(handler)
return &Logger{Logger: logger}
}
// NewLoggerWithOutput creates a new logger with custom output
func NewLoggerWithOutput(w io.Writer) *Logger {
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Customize timestamp format
if a.Key == slog.TimeKey {
return slog.Attr{
Key: a.Key,
Value: slog.StringValue(time.Now().Format("2006-01-02 15:04:05")),
}
}
return a
},
}
handler := slog.NewTextHandler(w, opts)
logger := slog.New(handler)
return &Logger{Logger: logger}
}
// NewSilentLogger creates a logger that discards all output
func NewSilentLogger() *Logger {
return NewLoggerWithOutput(io.Discard)
}
// WithComponent adds a component context to the logger
func (l *Logger) WithComponent(component string) *Logger {
return &Logger{Logger: l.Logger.With("component", component)}
}
// WithRequestID adds a request ID context to the logger
func (l *Logger) WithRequestID(requestID string) *Logger {
return &Logger{Logger: l.Logger.With("request_id", requestID)}
}
// Fatal logs a fatal error and exits the program
func (l *Logger) Fatal(msg string, args ...interface{}) {
l.Error(msg, args...)
os.Exit(1)
}