1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-31 10:56:24 -07:00
gochan/pkg/config/util.go

223 lines
5.5 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"os/user"
"path"
"runtime"
"strconv"
"testing"
"time"
"github.com/gochan-org/gochan/pkg/gcutil"
)
const (
InitialSetupStatusUnknown InitialSetupStatus = iota
InitialSetupNotStarted
InitialSetupComplete
DirFileMode fs.FileMode = 0775
NormalFileMode fs.FileMode = 0664
)
var (
uid int
gid int
standardConfigSearchPaths = []string{"gochan.json", "/usr/local/etc/gochan/gochan.json", "/etc/gochan/gochan.json"}
initialSetupStatus InitialSetupStatus = InitialSetupStatusUnknown
)
// MissingField represents a field missing from the configuration file
type MissingField struct {
Name string
Critical bool
Description string
}
// InvalidValueError represents a GochanConfig field with a bad value
type InvalidValueError struct {
Field string
Value any
Details string
}
func (iv *InvalidValueError) Error() string {
str := fmt.Sprintf("invalid %s value: %#v", iv.Field, iv.Value)
if iv.Details != "" {
str += " - " + iv.Details
}
return str
}
// GetGochanJSONPath returns the location of gochan.json, searching in the working directory,
// /usr/local/etc/gochan, and /etc/gochan in that order. If it is not found, it returns an empty string.
func GetGochanJSONPath() string {
if cfgPath != "" {
return cfgPath
}
return gcutil.FindResource(standardConfigSearchPaths...)
}
// GetUser returns the IDs of the user and group gochan should be acting as
// when creating files. If they are 0, it is using the current user
func GetUser() (int, int) {
return uid, gid
}
func TakeOwnership(fp string) (err error) {
if runtime.GOOS == "windows" || fp == "" || cfg.Username == "" {
// Chown returns an error in Windows so skip it, also skip if Username isn't set
// because otherwise it'll think we want to switch to uid and gid 0 (root)
return nil
}
return os.Chown(fp, uid, gid)
}
func TakeOwnershipOfFile(f *os.File) error {
if runtime.GOOS == "windows" || f == nil || cfg.Username == "" {
// Chown returns an error in Windows so skip it, also skip if Username isn't set
// because otherwise it'll think we want to switch to uid and gid 0 (root)
return nil
}
return f.Chown(uid, gid)
}
// SetSystemCriticalConfig sets system critical configuration values
func SetSystemCriticalConfig(systemCritical *SystemCriticalConfig) {
setDefaultCfgIfNotSet()
cfg.SystemCriticalConfig = *systemCritical
}
// SetSiteConfig sets the site configuration values
func SetSiteConfig(siteConfig *SiteConfig) {
setDefaultCfgIfNotSet()
cfg.SiteConfig = *siteConfig
}
func loadConfig() (err error) {
cfg = defaultGochanConfig
if testing.Testing() {
// create a dummy config for testing if we're using go test
cfg = defaultGochanConfig
cfg.ListenAddress = "127.0.0.1"
cfg.Port = 8080
cfg.UseFastCGI = true
cfg.TemplateDir = "templates"
cfg.LogDir = "log"
cfg.DBtype = "sqlite3"
cfg.DocumentRoot = "html"
cfg.DBhost = "./testdata/gochantest.db"
cfg.DBname = "gochan"
cfg.DBusername = "gochan"
cfg.SiteHost = "127.0.0.1"
cfg.RandomSeed = "test"
cfg.SiteSlogan = "Gochan testing"
cfg.Cooldowns = BoardCooldowns{0, 0, 0}
cfg.BanColors = map[string]string{
"admin": "#0000A0",
"somemod": "blue",
}
return
}
cfgPath = gcutil.FindResource(standardConfigSearchPaths...)
if cfgPath == "" {
return errors.New("gochan.json not found")
}
cfgBytes, err := os.ReadFile(cfgPath)
if err != nil {
return fmt.Errorf("error reading %s: %w", cfgPath, err)
}
if err = json.Unmarshal(cfgBytes, cfg); err != nil {
var unmarshalTypeError *json.UnmarshalTypeError
if errors.As(err, &unmarshalTypeError) {
return fmt.Errorf("invalid field type %s in %s: expected %s, found %s",
unmarshalTypeError.Field, cfgPath, unmarshalTypeError.Type, unmarshalTypeError.Value)
}
return fmt.Errorf("error parsing %s: %w", cfgPath, err)
}
cfg.jsonLocation = cfgPath
return nil
}
// InitConfig loads and parses gochan.json on startup and verifies its contents
func InitConfig() (err error) {
initialSetupStatus = InitialSetupNotStarted
if err = loadConfig(); err != nil {
return err
}
if err = cfg.ValidateValues(); err != nil {
return err
}
if runtime.GOOS != "windows" {
var gcUser *user.User
if cfg.Username != "" {
gcUser, err = user.Lookup(cfg.Username)
} else {
gcUser, err = user.Current()
}
if err != nil {
return err
}
if uid, err = strconv.Atoi(gcUser.Uid); err != nil {
return err
}
if gid, err = strconv.Atoi(gcUser.Gid); err != nil {
return err
}
}
if _, err = os.Stat(cfg.DocumentRoot); err != nil {
return err
}
if _, err = os.Stat(cfg.TemplateDir); err != nil {
return err
}
if _, err = os.Stat(cfg.LogDir); os.IsNotExist(err) {
err = os.MkdirAll(cfg.LogDir, DirFileMode)
}
if err != nil {
return err
}
cfg.LogDir = gcutil.FindResource(cfg.LogDir, "log", "/var/log/gochan/")
if cfg.Port == 0 {
cfg.Port = 80
}
if len(cfg.FirstPage) == 0 {
cfg.FirstPage = []string{"index.html", "1.html", "firstrun.html"}
}
if cfg.WebRoot == "" {
cfg.WebRoot = "/"
}
if cfg.WebRoot[0] != '/' {
cfg.WebRoot = "/" + cfg.WebRoot
}
if cfg.WebRoot[len(cfg.WebRoot)-1] != '/' {
cfg.WebRoot += "/"
}
_, zoneOffset := time.Now().Zone()
cfg.TimeZone = zoneOffset / 60 / 60
initialSetupStatus = InitialSetupComplete
return nil
}
// WebPath returns an absolute path, starting at the web root (which is "/" by default)
func WebPath(part ...string) string {
return path.Join(cfg.WebRoot, path.Join(part...))
}