mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 03:24:05 +00:00
395 lines
15 KiB
Go
395 lines
15 KiB
Go
// Package config handles application configuration using Viper.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/ivuorinen/gibidify/utils"
|
|
)
|
|
|
|
const (
|
|
// DefaultFileSizeLimit is the default maximum file size (5MB).
|
|
DefaultFileSizeLimit = 5242880
|
|
// MinFileSizeLimit is the minimum allowed file size limit (1KB).
|
|
MinFileSizeLimit = 1024
|
|
// MaxFileSizeLimit is the maximum allowed file size limit (100MB).
|
|
MaxFileSizeLimit = 104857600
|
|
)
|
|
|
|
// LoadConfig reads configuration from a YAML file.
|
|
// It looks for config in the following order:
|
|
// 1. $XDG_CONFIG_HOME/gibidify/config.yaml
|
|
// 2. $HOME/.config/gibidify/config.yaml
|
|
// 3. The current directory as fallback.
|
|
func LoadConfig() {
|
|
viper.SetConfigName("config")
|
|
viper.SetConfigType("yaml")
|
|
|
|
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
|
|
viper.AddConfigPath(filepath.Join(xdgConfig, "gibidify"))
|
|
} else if home, err := os.UserHomeDir(); err == nil {
|
|
viper.AddConfigPath(filepath.Join(home, ".config", "gibidify"))
|
|
}
|
|
// Only add current directory if no config file named gibidify.yaml exists
|
|
// to avoid conflicts with the project's output file
|
|
if _, err := os.Stat("gibidify.yaml"); os.IsNotExist(err) {
|
|
viper.AddConfigPath(".")
|
|
}
|
|
|
|
if err := viper.ReadInConfig(); err != nil {
|
|
logrus.Infof("Config file not found, using default values: %v", err)
|
|
setDefaultConfig()
|
|
} else {
|
|
logrus.Infof("Using config file: %s", viper.ConfigFileUsed())
|
|
// Validate configuration after loading
|
|
if err := ValidateConfig(); err != nil {
|
|
logrus.Warnf("Configuration validation failed: %v", err)
|
|
logrus.Info("Falling back to default configuration")
|
|
// Reset viper and set defaults when validation fails
|
|
viper.Reset()
|
|
setDefaultConfig()
|
|
}
|
|
}
|
|
}
|
|
|
|
// setDefaultConfig sets default configuration values.
|
|
func setDefaultConfig() {
|
|
viper.SetDefault("fileSizeLimit", DefaultFileSizeLimit)
|
|
// Default ignored directories.
|
|
viper.SetDefault("ignoreDirectories", []string{
|
|
"vendor", "node_modules", ".git", "dist", "build", "target", "bower_components", "cache", "tmp",
|
|
})
|
|
|
|
// FileTypeRegistry defaults
|
|
viper.SetDefault("fileTypes.enabled", true)
|
|
viper.SetDefault("fileTypes.customImageExtensions", []string{})
|
|
viper.SetDefault("fileTypes.customBinaryExtensions", []string{})
|
|
viper.SetDefault("fileTypes.customLanguages", map[string]string{})
|
|
viper.SetDefault("fileTypes.disabledImageExtensions", []string{})
|
|
viper.SetDefault("fileTypes.disabledBinaryExtensions", []string{})
|
|
viper.SetDefault("fileTypes.disabledLanguageExtensions", []string{})
|
|
|
|
// Back-pressure and memory management defaults
|
|
viper.SetDefault("backpressure.enabled", true)
|
|
viper.SetDefault("backpressure.maxPendingFiles", 1000) // Max files in file channel buffer
|
|
viper.SetDefault("backpressure.maxPendingWrites", 100) // Max writes in write channel buffer
|
|
viper.SetDefault("backpressure.maxMemoryUsage", 104857600) // 100MB max memory usage
|
|
viper.SetDefault("backpressure.memoryCheckInterval", 1000) // Check memory every 1000 files
|
|
}
|
|
|
|
// GetFileSizeLimit returns the file size limit from configuration.
|
|
func GetFileSizeLimit() int64 {
|
|
return viper.GetInt64("fileSizeLimit")
|
|
}
|
|
|
|
// GetIgnoredDirectories returns the list of directories to ignore.
|
|
func GetIgnoredDirectories() []string {
|
|
return viper.GetStringSlice("ignoreDirectories")
|
|
}
|
|
|
|
// ValidateConfig validates the loaded configuration.
|
|
func ValidateConfig() error {
|
|
var validationErrors []string
|
|
|
|
// Validate file size limit
|
|
fileSizeLimit := viper.GetInt64("fileSizeLimit")
|
|
if fileSizeLimit < MinFileSizeLimit {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileSizeLimit (%d) is below minimum (%d)", fileSizeLimit, MinFileSizeLimit))
|
|
}
|
|
if fileSizeLimit > MaxFileSizeLimit {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileSizeLimit (%d) exceeds maximum (%d)", fileSizeLimit, MaxFileSizeLimit))
|
|
}
|
|
|
|
// Validate ignore directories
|
|
ignoreDirectories := viper.GetStringSlice("ignoreDirectories")
|
|
for i, dir := range ignoreDirectories {
|
|
dir = strings.TrimSpace(dir)
|
|
if dir == "" {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] is empty", i))
|
|
continue
|
|
}
|
|
if strings.Contains(dir, "/") {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] (%s) contains path separator - only directory names are allowed", i, dir))
|
|
}
|
|
if strings.HasPrefix(dir, ".") && dir != ".git" && dir != ".vscode" && dir != ".idea" {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] (%s) starts with dot - this may cause unexpected behavior", i, dir))
|
|
}
|
|
}
|
|
|
|
// Validate supported output formats if configured
|
|
if viper.IsSet("supportedFormats") {
|
|
supportedFormats := viper.GetStringSlice("supportedFormats")
|
|
validFormats := map[string]bool{"json": true, "yaml": true, "markdown": true}
|
|
for i, format := range supportedFormats {
|
|
format = strings.ToLower(strings.TrimSpace(format))
|
|
if !validFormats[format] {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("supportedFormats[%d] (%s) is not a valid format (json, yaml, markdown)", i, format))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate concurrency settings if configured
|
|
if viper.IsSet("maxConcurrency") {
|
|
maxConcurrency := viper.GetInt("maxConcurrency")
|
|
if maxConcurrency < 1 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("maxConcurrency (%d) must be at least 1", maxConcurrency))
|
|
}
|
|
if maxConcurrency > 100 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("maxConcurrency (%d) is unreasonably high (max 100)", maxConcurrency))
|
|
}
|
|
}
|
|
|
|
// Validate file patterns if configured
|
|
if viper.IsSet("filePatterns") {
|
|
filePatterns := viper.GetStringSlice("filePatterns")
|
|
for i, pattern := range filePatterns {
|
|
pattern = strings.TrimSpace(pattern)
|
|
if pattern == "" {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("filePatterns[%d] is empty", i))
|
|
continue
|
|
}
|
|
// Basic validation - patterns should contain at least one alphanumeric character
|
|
if !strings.ContainsAny(pattern, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("filePatterns[%d] (%s) appears to be invalid", i, pattern))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate FileTypeRegistry configuration
|
|
if viper.IsSet("fileTypes.customImageExtensions") {
|
|
customImages := viper.GetStringSlice("fileTypes.customImageExtensions")
|
|
for i, ext := range customImages {
|
|
ext = strings.TrimSpace(ext)
|
|
if ext == "" {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customImageExtensions[%d] is empty", i))
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(ext, ".") {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customImageExtensions[%d] (%s) must start with a dot", i, ext))
|
|
}
|
|
}
|
|
}
|
|
|
|
if viper.IsSet("fileTypes.customBinaryExtensions") {
|
|
customBinary := viper.GetStringSlice("fileTypes.customBinaryExtensions")
|
|
for i, ext := range customBinary {
|
|
ext = strings.TrimSpace(ext)
|
|
if ext == "" {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customBinaryExtensions[%d] is empty", i))
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(ext, ".") {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customBinaryExtensions[%d] (%s) must start with a dot", i, ext))
|
|
}
|
|
}
|
|
}
|
|
|
|
if viper.IsSet("fileTypes.customLanguages") {
|
|
customLangs := viper.GetStringMapString("fileTypes.customLanguages")
|
|
for ext, lang := range customLangs {
|
|
ext = strings.TrimSpace(ext)
|
|
lang = strings.TrimSpace(lang)
|
|
if ext == "" {
|
|
validationErrors = append(validationErrors, "fileTypes.customLanguages contains empty extension key")
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(ext, ".") {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customLanguages extension (%s) must start with a dot", ext))
|
|
}
|
|
if lang == "" {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customLanguages[%s] has empty language value", ext))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate back-pressure configuration
|
|
if viper.IsSet("backpressure.maxPendingFiles") {
|
|
maxPendingFiles := viper.GetInt("backpressure.maxPendingFiles")
|
|
if maxPendingFiles < 1 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingFiles (%d) must be at least 1", maxPendingFiles))
|
|
}
|
|
if maxPendingFiles > 100000 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingFiles (%d) is unreasonably high (max 100000)", maxPendingFiles))
|
|
}
|
|
}
|
|
|
|
if viper.IsSet("backpressure.maxPendingWrites") {
|
|
maxPendingWrites := viper.GetInt("backpressure.maxPendingWrites")
|
|
if maxPendingWrites < 1 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingWrites (%d) must be at least 1", maxPendingWrites))
|
|
}
|
|
if maxPendingWrites > 10000 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingWrites (%d) is unreasonably high (max 10000)", maxPendingWrites))
|
|
}
|
|
}
|
|
|
|
if viper.IsSet("backpressure.maxMemoryUsage") {
|
|
maxMemoryUsage := viper.GetInt64("backpressure.maxMemoryUsage")
|
|
if maxMemoryUsage < 1048576 { // 1MB minimum
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxMemoryUsage (%d) must be at least 1MB (1048576 bytes)", maxMemoryUsage))
|
|
}
|
|
if maxMemoryUsage > 10737418240 { // 10GB maximum
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxMemoryUsage (%d) is unreasonably high (max 10GB)", maxMemoryUsage))
|
|
}
|
|
}
|
|
|
|
if viper.IsSet("backpressure.memoryCheckInterval") {
|
|
interval := viper.GetInt("backpressure.memoryCheckInterval")
|
|
if interval < 1 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.memoryCheckInterval (%d) must be at least 1", interval))
|
|
}
|
|
if interval > 100000 {
|
|
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.memoryCheckInterval (%d) is unreasonably high (max 100000)", interval))
|
|
}
|
|
}
|
|
|
|
if len(validationErrors) > 0 {
|
|
return utils.NewStructuredError(
|
|
utils.ErrorTypeConfiguration,
|
|
utils.CodeConfigValidation,
|
|
"configuration validation failed: "+strings.Join(validationErrors, "; "),
|
|
).WithContext("validation_errors", validationErrors)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMaxConcurrency returns the maximum concurrency limit from configuration.
|
|
func GetMaxConcurrency() int {
|
|
return viper.GetInt("maxConcurrency")
|
|
}
|
|
|
|
// GetSupportedFormats returns the supported output formats from configuration.
|
|
func GetSupportedFormats() []string {
|
|
return viper.GetStringSlice("supportedFormats")
|
|
}
|
|
|
|
// GetFilePatterns returns the file patterns from configuration.
|
|
func GetFilePatterns() []string {
|
|
return viper.GetStringSlice("filePatterns")
|
|
}
|
|
|
|
// IsValidFormat checks if a format is supported.
|
|
func IsValidFormat(format string) bool {
|
|
format = strings.ToLower(strings.TrimSpace(format))
|
|
validFormats := map[string]bool{"json": true, "yaml": true, "markdown": true}
|
|
return validFormats[format]
|
|
}
|
|
|
|
// ValidateFileSize checks if a file size is within the configured limit.
|
|
func ValidateFileSize(size int64) error {
|
|
limit := GetFileSizeLimit()
|
|
if size > limit {
|
|
return utils.NewStructuredError(
|
|
utils.ErrorTypeValidation,
|
|
utils.CodeValidationSize,
|
|
fmt.Sprintf("file size (%d bytes) exceeds limit (%d bytes)", size, limit),
|
|
).WithContext("file_size", size).WithContext("size_limit", limit)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateOutputFormat checks if an output format is valid.
|
|
func ValidateOutputFormat(format string) error {
|
|
if !IsValidFormat(format) {
|
|
return utils.NewStructuredError(
|
|
utils.ErrorTypeValidation,
|
|
utils.CodeValidationFormat,
|
|
fmt.Sprintf("unsupported output format: %s (supported: json, yaml, markdown)", format),
|
|
).WithContext("format", format)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateConcurrency checks if a concurrency level is valid.
|
|
func ValidateConcurrency(concurrency int) error {
|
|
if concurrency < 1 {
|
|
return utils.NewStructuredError(
|
|
utils.ErrorTypeValidation,
|
|
utils.CodeValidationFormat,
|
|
fmt.Sprintf("concurrency (%d) must be at least 1", concurrency),
|
|
).WithContext("concurrency", concurrency)
|
|
}
|
|
|
|
if viper.IsSet("maxConcurrency") {
|
|
maxConcurrency := GetMaxConcurrency()
|
|
if concurrency > maxConcurrency {
|
|
return utils.NewStructuredError(
|
|
utils.ErrorTypeValidation,
|
|
utils.CodeValidationFormat,
|
|
fmt.Sprintf("concurrency (%d) exceeds maximum (%d)", concurrency, maxConcurrency),
|
|
).WithContext("concurrency", concurrency).WithContext("max_concurrency", maxConcurrency)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetFileTypesEnabled returns whether file type detection is enabled.
|
|
func GetFileTypesEnabled() bool {
|
|
return viper.GetBool("fileTypes.enabled")
|
|
}
|
|
|
|
// GetCustomImageExtensions returns custom image extensions from configuration.
|
|
func GetCustomImageExtensions() []string {
|
|
return viper.GetStringSlice("fileTypes.customImageExtensions")
|
|
}
|
|
|
|
// GetCustomBinaryExtensions returns custom binary extensions from configuration.
|
|
func GetCustomBinaryExtensions() []string {
|
|
return viper.GetStringSlice("fileTypes.customBinaryExtensions")
|
|
}
|
|
|
|
// GetCustomLanguages returns custom language mappings from configuration.
|
|
func GetCustomLanguages() map[string]string {
|
|
return viper.GetStringMapString("fileTypes.customLanguages")
|
|
}
|
|
|
|
// GetDisabledImageExtensions returns disabled image extensions from configuration.
|
|
func GetDisabledImageExtensions() []string {
|
|
return viper.GetStringSlice("fileTypes.disabledImageExtensions")
|
|
}
|
|
|
|
// GetDisabledBinaryExtensions returns disabled binary extensions from configuration.
|
|
func GetDisabledBinaryExtensions() []string {
|
|
return viper.GetStringSlice("fileTypes.disabledBinaryExtensions")
|
|
}
|
|
|
|
// GetDisabledLanguageExtensions returns disabled language extensions from configuration.
|
|
func GetDisabledLanguageExtensions() []string {
|
|
return viper.GetStringSlice("fileTypes.disabledLanguageExtensions")
|
|
}
|
|
|
|
// Back-pressure configuration getters
|
|
|
|
// GetBackpressureEnabled returns whether back-pressure management is enabled.
|
|
func GetBackpressureEnabled() bool {
|
|
return viper.GetBool("backpressure.enabled")
|
|
}
|
|
|
|
// GetMaxPendingFiles returns the maximum number of files that can be pending in the file channel.
|
|
func GetMaxPendingFiles() int {
|
|
return viper.GetInt("backpressure.maxPendingFiles")
|
|
}
|
|
|
|
// GetMaxPendingWrites returns the maximum number of writes that can be pending in the write channel.
|
|
func GetMaxPendingWrites() int {
|
|
return viper.GetInt("backpressure.maxPendingWrites")
|
|
}
|
|
|
|
// GetMaxMemoryUsage returns the maximum memory usage in bytes before back-pressure kicks in.
|
|
func GetMaxMemoryUsage() int64 {
|
|
return viper.GetInt64("backpressure.maxMemoryUsage")
|
|
}
|
|
|
|
// GetMemoryCheckInterval returns how often to check memory usage (in number of files processed).
|
|
func GetMemoryCheckInterval() int {
|
|
return viper.GetInt("backpressure.memoryCheckInterval")
|
|
}
|