Files
gobsidian/internal/config/config.go
2025-08-25 21:19:15 +01:00

369 lines
10 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"gopkg.in/ini.v1"
)
type Config struct {
// SERVER 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
// Visibility settings
ShowImagesInTree bool
ShowFilesInTree bool
ShowImagesInFolder bool
ShowFilesInFolder bool
// Database settings
DBType string
DBPath string
// Auth settings
RequireAdminActivation bool
RequireEmailConfirmation bool
MFAEnabledByDefault bool
// Email (SMTP) settings
SMTPHost string
SMTPPort int
SMTPUsername string
SMTPPassword string
SMTPSender string
SMTPUseTLS bool
}
var defaultConfig = map[string]map[string]string{
"SERVER": {
"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",
// Visibility defaults
"SHOW_IMAGES_IN_TREE": "false",
"SHOW_FILES_IN_TREE": "true",
"SHOW_IMAGES_IN_FOLDER": "true",
"SHOW_FILES_IN_FOLDER": "true",
},
"DATABASE": {
"TYPE": "sqlite",
"PATH": "data/gobsidian.db",
},
"AUTH": {
"REQUIRE_ADMIN_ACTIVATION": "true",
"REQUIRE_EMAIL_CONFIRMATION": "true",
"MFA_ENABLED_BY_DEFAULT": "false",
},
"EMAIL": {
"SMTP_HOST": "",
"SMTP_PORT": "587",
"SMTP_USERNAME": "",
"SMTP_PASSWORD": "",
"SMTP_SENDER": "",
"SMTP_USE_TLS": "true",
},
}
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 SERVER section
SERVERSection := cfg.Section("SERVER")
config.Host = SERVERSection.Key("HOST").String()
config.Port, _ = SERVERSection.Key("PORT").Int()
config.SecretKey = SERVERSection.Key("SECRET_KEY").String()
config.Debug, _ = SERVERSection.Key("DEBUG").Bool()
maxContentMB, _ := SERVERSection.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()
// New visibility flags (fallback to legacy IMAGES_HIDE for folder images if keys missing)
config.ShowImagesInTree, _ = notesSection.Key("SHOW_IMAGES_IN_TREE").Bool()
config.ShowFilesInTree, _ = notesSection.Key("SHOW_FILES_IN_TREE").Bool()
if key := notesSection.Key("SHOW_IMAGES_IN_FOLDER"); key.String() != "" {
config.ShowImagesInFolder, _ = key.Bool()
} else {
// fallback: if IMAGES_HIDE true => not shown
config.ShowImagesInFolder = !config.ImagesHide
}
if key := notesSection.Key("SHOW_FILES_IN_FOLDER"); key.String() != "" {
config.ShowFilesInFolder, _ = key.Bool()
} else {
config.ShowFilesInFolder = true
}
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)
}
// Load DATABASE section
dbSection := cfg.Section("DATABASE")
config.DBType = strings.ToLower(strings.TrimSpace(dbSection.Key("TYPE").String()))
config.DBPath = dbSection.Key("PATH").String()
if config.DBType == "sqlite" {
if !filepath.IsAbs(config.DBPath) {
wd, _ := os.Getwd()
config.DBPath = filepath.Join(wd, config.DBPath)
}
// ensure parent dir exists
if err := os.MkdirAll(filepath.Dir(config.DBPath), 0o755); err != nil {
return nil, fmt.Errorf("failed to create db directory: %w", err)
}
}
// Load AUTH section
authSection := cfg.Section("AUTH")
config.RequireAdminActivation, _ = authSection.Key("REQUIRE_ADMIN_ACTIVATION").Bool()
config.RequireEmailConfirmation, _ = authSection.Key("REQUIRE_EMAIL_CONFIRMATION").Bool()
config.MFAEnabledByDefault, _ = authSection.Key("MFA_ENABLED_BY_DEFAULT").Bool()
// Load EMAIL (SMTP) section
emailSection := cfg.Section("EMAIL")
config.SMTPHost = emailSection.Key("SMTP_HOST").String()
config.SMTPPort, _ = emailSection.Key("SMTP_PORT").Int()
config.SMTPUsername = emailSection.Key("SMTP_USERNAME").String()
config.SMTPPassword = emailSection.Key("SMTP_PASSWORD").String()
config.SMTPSender = emailSection.Key("SMTP_SENDER").String()
config.SMTPUseTLS, _ = emailSection.Key("SMTP_USE_TLS").Bool()
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 "SERVER":
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)
case "SHOW_IMAGES_IN_TREE":
c.ShowImagesInTree = value == "true"
case "SHOW_FILES_IN_TREE":
c.ShowFilesInTree = value == "true"
case "SHOW_IMAGES_IN_FOLDER":
c.ShowImagesInFolder = value == "true"
case "SHOW_FILES_IN_FOLDER":
c.ShowFilesInFolder = value == "true"
}
case "DATABASE":
switch key {
case "TYPE":
c.DBType = strings.ToLower(strings.TrimSpace(value))
case "PATH":
c.DBPath = value
}
case "AUTH":
switch key {
case "REQUIRE_ADMIN_ACTIVATION":
c.RequireAdminActivation = value == "true"
case "REQUIRE_EMAIL_CONFIRMATION":
c.RequireEmailConfirmation = value == "true"
case "MFA_ENABLED_BY_DEFAULT":
c.MFAEnabledByDefault = value == "true"
}
case "EMAIL":
switch key {
case "SMTP_HOST":
c.SMTPHost = value
case "SMTP_PORT":
if v, err := strconv.Atoi(value); err == nil {
c.SMTPPort = v
}
case "SMTP_USERNAME":
c.SMTPUsername = value
case "SMTP_PASSWORD":
c.SMTPPassword = value
case "SMTP_SENDER":
c.SMTPSender = value
case "SMTP_USE_TLS":
c.SMTPUseTLS = value == "true"
}
}
return cfg.SaveTo(configPath)
}