// Package config handles loading and validation of application configuration. package config import ( "fmt" "os" "path/filepath" "gopkg.in/ini.v1" ) // Config holds all application configuration. type Config struct { Server ServerConfig Calendar CalendarConfig Diary DiaryConfig CalDAV CalDAVConfig Photos PhotosConfig } // ServerConfig holds HTTP server settings. type ServerConfig struct { Port int } // CalendarConfig holds calendar ICS file settings. type CalendarConfig struct { LocalICS string } // DiaryConfig holds diary ICS file settings. type DiaryConfig struct { ICSFile string } // CalDAVConfig holds CalDAV remote calendar settings. type CalDAVConfig struct { Enabled bool URL string Username string Password string } // PhotosConfig holds photo storage settings. type PhotosConfig struct { RootFolder string } // Load reads configuration from the specified INI file. func Load(path string) (*Config, error) { cfg, err := ini.Load(path) if err != nil { return nil, fmt.Errorf("failed to load config file: %w", err) } config := &Config{ Server: ServerConfig{ Port: cfg.Section("server").Key("port").MustInt(8080), }, Calendar: CalendarConfig{ LocalICS: cfg.Section("calendar").Key("local_ics").String(), }, Diary: DiaryConfig{ ICSFile: cfg.Section("diary").Key("ics_file").String(), }, CalDAV: CalDAVConfig{ Enabled: cfg.Section("caldav").Key("enabled").MustBool(false), URL: cfg.Section("caldav").Key("url").String(), Username: cfg.Section("caldav").Key("username").String(), Password: cfg.Section("caldav").Key("password").String(), }, Photos: PhotosConfig{ RootFolder: cfg.Section("photos").Key("root_folder").String(), }, } return config, nil } // Validate checks that all required configuration values are set and valid. func (c *Config) Validate() error { if c.Server.Port <= 0 || c.Server.Port > 65535 { return fmt.Errorf("invalid server port: %d", c.Server.Port) } // Validate calendar ICS path if specified if c.Calendar.LocalICS != "" { if err := validateFileReadable(c.Calendar.LocalICS); err != nil { return fmt.Errorf("calendar ICS file: %w", err) } } // Validate diary ICS path if specified if c.Diary.ICSFile != "" { dir := filepath.Dir(c.Diary.ICSFile) if err := validateDirExists(dir); err != nil { return fmt.Errorf("diary ICS directory: %w", err) } } // Validate CalDAV settings if enabled if c.CalDAV.Enabled { if c.CalDAV.URL == "" { return fmt.Errorf("caldav URL is required when caldav is enabled") } } // Validate photos folder if specified if c.Photos.RootFolder != "" { if err := validateDirExists(c.Photos.RootFolder); err != nil { return fmt.Errorf("photos root folder: %w", err) } } return nil } func validateFileReadable(path string) error { info, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { return fmt.Errorf("file does not exist: %s", path) } return fmt.Errorf("cannot access file: %w", err) } if info.IsDir() { return fmt.Errorf("path is a directory, not a file: %s", path) } return nil } func validateDirExists(path string) error { info, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { return fmt.Errorf("directory does not exist: %s", path) } return fmt.Errorf("cannot access directory: %w", err) } if !info.IsDir() { return fmt.Errorf("path is not a directory: %s", path) } return nil }