package config import ( "fmt" "os" "path/filepath" "strconv" "strings" "gopkg.in/ini.v1" ) type Config struct { // Flask equivalent settings Host string Port int SecretKey string Debug bool MaxContentLength int64 // in bytes // MD Notes App settings AppName string NotesDir string NotesDirHideSidepane []string NotesDirSkip []string ImagesHide bool ImageStorageMode int ImageStoragePath string ImageSubfolderName string AllowedImageExtensions []string AllowedFileExtensions []string } var defaultConfig = map[string]map[string]string{ "FLASK": { "HOST": "0.0.0.0", "PORT": "3000", "SECRET_KEY": "change-this-secret-key", "DEBUG": "false", "MAX_CONTENT_LENGTH": "16", // in MB }, "MD_NOTES_APP": { "APP_NAME": "Gobsidian", "NOTES_DIR": "notes", "NOTES_DIR_HIDE_SIDEPANE": "attached, images", "NOTES_DIR_SKIP": "secret, private", "IMAGES_HIDE": "false", "IMAGE_STORAGE_MODE": "1", "IMAGE_STORAGE_PATH": "images", "IMAGE_SUBFOLDER_NAME": "attached", "ALLOWED_IMAGE_EXTENSIONS": "jpg, jpeg, png, webp, gif", "ALLOWED_FILE_EXTENSIONS": "txt, pdf, html, json, yaml, yml, conf, csv, cmd, bat, sh", }, } func Load() (*Config, error) { configPath := "settings.ini" // Ensure config file exists if err := ensureConfigFile(configPath); err != nil { return nil, fmt.Errorf("failed to ensure config file: %w", err) } // Load configuration cfg, err := ini.Load(configPath) if err != nil { return nil, fmt.Errorf("failed to load config file: %w", err) } config := &Config{} // Load FLASK section flaskSection := cfg.Section("FLASK") config.Host = flaskSection.Key("HOST").String() config.Port, _ = flaskSection.Key("PORT").Int() config.SecretKey = flaskSection.Key("SECRET_KEY").String() config.Debug, _ = flaskSection.Key("DEBUG").Bool() maxContentMB, _ := flaskSection.Key("MAX_CONTENT_LENGTH").Int() config.MaxContentLength = int64(maxContentMB) * 1024 * 1024 // Convert MB to bytes // Load MD_NOTES_APP section notesSection := cfg.Section("MD_NOTES_APP") config.AppName = notesSection.Key("APP_NAME").String() config.NotesDir = notesSection.Key("NOTES_DIR").String() // Parse comma-separated lists config.NotesDirHideSidepane = parseCommaSeparated(notesSection.Key("NOTES_DIR_HIDE_SIDEPANE").String()) config.NotesDirSkip = parseCommaSeparated(notesSection.Key("NOTES_DIR_SKIP").String()) config.AllowedImageExtensions = parseCommaSeparated(notesSection.Key("ALLOWED_IMAGE_EXTENSIONS").String()) config.AllowedFileExtensions = parseCommaSeparated(notesSection.Key("ALLOWED_FILE_EXTENSIONS").String()) config.ImagesHide, _ = notesSection.Key("IMAGES_HIDE").Bool() config.ImageStorageMode, _ = notesSection.Key("IMAGE_STORAGE_MODE").Int() config.ImageStoragePath = notesSection.Key("IMAGE_STORAGE_PATH").String() config.ImageSubfolderName = notesSection.Key("IMAGE_SUBFOLDER_NAME").String() // Convert relative paths to absolute if !filepath.IsAbs(config.NotesDir) { wd, _ := os.Getwd() config.NotesDir = filepath.Join(wd, config.NotesDir) } if !filepath.IsAbs(config.ImageStoragePath) && config.ImageStorageMode == 2 { wd, _ := os.Getwd() config.ImageStoragePath = filepath.Join(wd, config.ImageStoragePath) } return config, nil } func ensureConfigFile(configPath string) error { // Check if file exists if _, err := os.Stat(configPath); os.IsNotExist(err) { return createDefaultConfigFile(configPath) } // File exists, check if it has all required settings return updateConfigFile(configPath) } func createDefaultConfigFile(configPath string) error { cfg := ini.Empty() for sectionName, settings := range defaultConfig { section, err := cfg.NewSection(sectionName) if err != nil { return err } for key, value := range settings { section.NewKey(key, value) } } return cfg.SaveTo(configPath) } func updateConfigFile(configPath string) error { cfg, err := ini.Load(configPath) if err != nil { return err } updated := false for sectionName, settings := range defaultConfig { section := cfg.Section(sectionName) for key, defaultValue := range settings { if !section.HasKey(key) { section.NewKey(key, defaultValue) updated = true } } } if updated { return cfg.SaveTo(configPath) } return nil } func parseCommaSeparated(value string) []string { if value == "" { return []string{} } parts := strings.Split(value, ",") result := make([]string, 0, len(parts)) for _, part := range parts { if trimmed := strings.TrimSpace(part); trimmed != "" { result = append(result, trimmed) } } return result } func (c *Config) SaveSetting(section, key, value string) error { configPath := "settings.ini" cfg, err := ini.Load(configPath) if err != nil { return err } sec := cfg.Section(section) sec.Key(key).SetValue(value) // Update in-memory config based on section and key switch section { case "FLASK": switch key { case "HOST": c.Host = value case "PORT": if port, err := strconv.Atoi(value); err == nil { c.Port = port } case "SECRET_KEY": c.SecretKey = value case "DEBUG": c.Debug = value == "true" case "MAX_CONTENT_LENGTH": if size, err := strconv.ParseInt(value, 10, 64); err == nil { c.MaxContentLength = size * 1024 * 1024 } } case "MD_NOTES_APP": switch key { case "APP_NAME": c.AppName = value case "NOTES_DIR": c.NotesDir = value case "NOTES_DIR_HIDE_SIDEPANE": c.NotesDirHideSidepane = parseCommaSeparated(value) case "NOTES_DIR_SKIP": c.NotesDirSkip = parseCommaSeparated(value) case "IMAGES_HIDE": c.ImagesHide = value == "true" case "IMAGE_STORAGE_MODE": if mode, err := strconv.Atoi(value); err == nil { c.ImageStorageMode = mode } case "IMAGE_STORAGE_PATH": c.ImageStoragePath = value case "IMAGE_SUBFOLDER_NAME": c.ImageSubfolderName = value case "ALLOWED_IMAGE_EXTENSIONS": c.AllowedImageExtensions = parseCommaSeparated(value) case "ALLOWED_FILE_EXTENSIONS": c.AllowedFileExtensions = parseCommaSeparated(value) } } return cfg.SaveTo(configPath) }