giredore/internal/configuration/fileconfig.go

205 lines
5.7 KiB
Go

package configuration
import (
// stdlib
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
// local
"go.dev.pztrn.name/giredore/internal/structs"
)
// This structure represents configuration that will be parsed via file.
// Despite on exporting fields there are setters and getters defined because
// data from configuration file will be parsed in exported fields and they
// may be accesses concurrently. In other words DO NOT USE EXPORTED FIELDS
// DIRECTLY!
type fileConfig struct {
// HTTP describes HTTP server configuration.
HTTP struct {
// AllowedIPs is a list of IPs that allowed to access API.
// There might be other authentication implemented in future.
AllowedIPs []string
allowedipsmutex sync.RWMutex
// Listen is an address on which HTTP server will listen.
Listen string
// WaitForSeconds is a timeout during which we will wait for
// HTTP server be up. If timeout will pass and HTTP server won't
// start processing requests - giredore will exit.
WaitForSeconds int
}
// Packages describes packages mapping.
Packages map[string]*structs.Package
packagesMutex sync.RWMutex
}
func (fc *fileConfig) AddOrUpdatePackage(pkg *structs.Package) {
fc.packagesMutex.Lock()
fc.Packages[pkg.OriginalPath] = pkg
fc.packagesMutex.Unlock()
}
func (fc *fileConfig) DeletePackage(req *structs.PackageDeleteRequest) []structs.Error {
var errors []structs.Error
fc.packagesMutex.Lock()
defer fc.packagesMutex.Unlock()
_, found := fc.Packages[req.OriginalPath]
if !found {
errors = append(errors, structs.ErrPackageWasntDefined)
return errors
}
delete(fc.Packages, req.OriginalPath)
return errors
}
func (fc *fileConfig) GetAllowedIPs() []string {
var allowedIPs []string
fc.HTTP.allowedipsmutex.RLock()
allowedIPs = append(allowedIPs, fc.HTTP.AllowedIPs...)
fc.HTTP.allowedipsmutex.RUnlock()
return allowedIPs
}
func (fc *fileConfig) GetAllPackagesInfo() map[string]*structs.Package {
pkgs := make(map[string]*structs.Package)
fc.packagesMutex.Lock()
for name, pkg := range fc.Packages {
pkgs[name] = pkg
}
fc.packagesMutex.Unlock()
return pkgs
}
func (fc *fileConfig) GetPackagesInfo(packages []string) (map[string]*structs.Package, []structs.Error) {
pkgs := make(map[string]*structs.Package)
var errors []structs.Error
fc.packagesMutex.Lock()
for _, neededPkg := range packages {
pkgData, found := fc.Packages[neededPkg]
if !found {
errors = append(errors, structs.ErrPackageWasntDefined+structs.Error(" Package was: "+neededPkg))
} else {
pkgs[neededPkg] = pkgData
}
}
fc.packagesMutex.Unlock()
return pkgs, errors
}
// Initialize parses file contents into structure.
func (fc *fileConfig) Initialize() {
configPath := filepath.Join(envCfg.DataDir, "config.json")
cfgLoadLog := log.With().Str("configuration path", configPath).Logger()
cfgLoadLog.Info().Msg("Loading configuration file...")
configPath, err := fc.normalizePath(configPath)
if err != nil {
cfgLoadLog.Fatal().Err(err).Msg("Failed to normalize configuration file path.")
}
// Check if file "config.json" specified in envConfig.DataDir field
// exists.
if _, err2 := os.Stat(configPath); os.IsNotExist(err2) {
cfgLoadLog.Error().Msg("Unable to load configuration from filesystem.")
return
}
// Try to load file into memory.
fileData, err3 := ioutil.ReadFile(configPath)
if err3 != nil {
cfgLoadLog.Fatal().Err(err3).Msg("Failed to read configuration file data into memory.")
}
// ...and parse it.
err4 := json.Unmarshal(fileData, fc)
if err4 != nil {
cfgLoadLog.Fatal().Err(err4).Msg("Failed to parse configuration file.")
}
if fc.Packages == nil {
fc.Packages = make(map[string]*structs.Package)
}
// Ensure that localhost (127.0.0.1) are defined in AllowedIPs.
var localhostIsAllowed bool
for _, ip := range fc.HTTP.AllowedIPs {
if strings.Contains(ip, "127.0.0.1") {
localhostIsAllowed = true
break
}
}
if !localhostIsAllowed {
cfgLoadLog.Warn().Msg("Localhost (127.0.0.1) wasn't allowed to access configuration API, adding it to list of allowed IP addresses")
fc.HTTP.AllowedIPs = append(fc.HTTP.AllowedIPs, "127.0.0.1")
} else {
cfgLoadLog.Debug().Msg("Localhost (127.0.0.1) is allowed to access configuration API")
}
cfgLoadLog.Debug().Msgf("Configuration parsed: %+v", fc)
cfgLoadLog.Info().Int("packages count", len(fc.Packages)).Msg("Packages list loaded")
}
// Normalizes passed configuration file path.
func (fc *fileConfig) normalizePath(configPath string) (string, error) {
// Normalize configuration file path.
if strings.Contains(configPath, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
configPath = strings.Replace(configPath, "~", homeDir, 1)
}
absPath, err1 := filepath.Abs(configPath)
if err1 != nil {
return "", err1
}
return absPath, nil
}
// Save saves configuration into file.
func (fc *fileConfig) Save() {
configPath := filepath.Join(envCfg.DataDir, "config.json")
cfgSaveLog := log.With().Str("configuration path", configPath).Logger()
cfgSaveLog.Info().Msg("Saving configuration file...")
data, err := json.Marshal(fc)
if err != nil {
cfgSaveLog.Fatal().Err(err).Msg("Failed to encode data into JSON. Configuration file won't be saved!")
return
}
configPath, err1 := fc.normalizePath(configPath)
if err1 != nil {
cfgSaveLog.Fatal().Err(err1).Msg("Failed to normalize configuration file path.")
}
err2 := ioutil.WriteFile(configPath, data, os.ModePerm)
if err2 != nil {
cfgSaveLog.Fatal().Err(err2).Msg("Failed to write configuration file data to file.")
}
cfgSaveLog.Info().Msg("Configuration file saved.")
}
func (fc *fileConfig) SetAllowedIPs(allowedIPs []string) {
fc.HTTP.allowedipsmutex.Lock()
fc.HTTP.AllowedIPs = allowedIPs
fc.HTTP.allowedipsmutex.Unlock()
}