440 lines
12 KiB
Go
440 lines
12 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"seanime/internal/constants"
|
|
"seanime/internal/util"
|
|
"strconv"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type Config struct {
|
|
Version string
|
|
Server struct {
|
|
Host string
|
|
Port int
|
|
Offline bool
|
|
UseBinaryPath bool // Makes $SEANIME_WORKING_DIR point to the binary's directory
|
|
Systray bool
|
|
DoHUrl string
|
|
Password string
|
|
}
|
|
Database struct {
|
|
Name string
|
|
}
|
|
Web struct {
|
|
AssetDir string
|
|
}
|
|
Logs struct {
|
|
Dir string
|
|
}
|
|
Cache struct {
|
|
Dir string
|
|
TranscodeDir string
|
|
}
|
|
Offline struct {
|
|
Dir string
|
|
AssetDir string
|
|
}
|
|
Manga struct {
|
|
DownloadDir string
|
|
LocalDir string
|
|
}
|
|
Data struct { // Hydrated after config is loaded
|
|
AppDataDir string
|
|
WorkingDir string
|
|
}
|
|
Extensions struct {
|
|
Dir string
|
|
}
|
|
Anilist struct {
|
|
ClientID string
|
|
}
|
|
Experimental struct {
|
|
MainServerTorrentStreaming bool
|
|
}
|
|
}
|
|
|
|
type ConfigOptions struct {
|
|
DataDir string // The path to the Seanime data directory, if any
|
|
OnVersionChange []func(oldVersion string, newVersion string)
|
|
EmbeddedLogo []byte // The embedded logo
|
|
IsDesktopSidecar bool // Run as the desktop sidecar
|
|
}
|
|
|
|
// NewConfig initializes the config
|
|
func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) {
|
|
|
|
logger.Debug().Msg("app: Initializing config")
|
|
|
|
// Set Seanime's environment variables
|
|
if os.Getenv("SEANIME_DATA_DIR") != "" {
|
|
options.DataDir = os.Getenv("SEANIME_DATA_DIR")
|
|
}
|
|
|
|
defaultHost := "127.0.0.1"
|
|
defaultPort := 43211
|
|
|
|
if os.Getenv("SEANIME_SERVER_HOST") != "" {
|
|
defaultHost = os.Getenv("SEANIME_SERVER_HOST")
|
|
}
|
|
if os.Getenv("SEANIME_SERVER_PORT") != "" {
|
|
var err error
|
|
defaultPort, err = strconv.Atoi(os.Getenv("SEANIME_SERVER_PORT"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SEANIME_SERVER_PORT environment variable: %s", os.Getenv("SEANIME_SERVER_PORT"))
|
|
}
|
|
}
|
|
|
|
// Initialize the app data directory
|
|
dataDir, configPath, err := initAppDataDir(options.DataDir, logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set Seanime's default custom environment variables
|
|
if err = setDataDirEnv(dataDir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Configure viper
|
|
viper.SetConfigName(constants.ConfigFileName)
|
|
viper.SetConfigType("toml")
|
|
viper.SetConfigFile(configPath)
|
|
|
|
// Set default values
|
|
viper.SetDefault("version", constants.Version)
|
|
viper.SetDefault("server.host", defaultHost)
|
|
viper.SetDefault("server.port", defaultPort)
|
|
viper.SetDefault("server.offline", false)
|
|
// Use the binary's directory as the working directory environment variable on macOS
|
|
viper.SetDefault("server.useBinaryPath", true)
|
|
//viper.SetDefault("server.systray", true)
|
|
viper.SetDefault("database.name", "seanime")
|
|
viper.SetDefault("web.assetDir", "$SEANIME_DATA_DIR/assets")
|
|
viper.SetDefault("cache.dir", "$SEANIME_DATA_DIR/cache")
|
|
viper.SetDefault("cache.transcodeDir", "$SEANIME_DATA_DIR/cache/transcode")
|
|
viper.SetDefault("manga.downloadDir", "$SEANIME_DATA_DIR/manga")
|
|
viper.SetDefault("manga.localDir", "$SEANIME_DATA_DIR/manga-local")
|
|
viper.SetDefault("logs.dir", "$SEANIME_DATA_DIR/logs")
|
|
viper.SetDefault("offline.dir", "$SEANIME_DATA_DIR/offline")
|
|
viper.SetDefault("offline.assetDir", "$SEANIME_DATA_DIR/offline/assets")
|
|
viper.SetDefault("extensions.dir", "$SEANIME_DATA_DIR/extensions")
|
|
|
|
// Create and populate the config file if it doesn't exist
|
|
if err = createConfigFile(configPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read the config file
|
|
if err := viper.ReadInConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Unmarshal the config values
|
|
cfg := &Config{}
|
|
if err := viper.Unmarshal(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update the config if the version has changed
|
|
if err := updateVersion(cfg, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Before expanding the values, check if we need to override the working directory
|
|
if err = setWorkingDirEnv(cfg.Server.UseBinaryPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Expand the values, replacing environment variables
|
|
expandEnvironmentValues(cfg)
|
|
cfg.Data.AppDataDir = dataDir
|
|
cfg.Data.WorkingDir = os.Getenv("SEANIME_WORKING_DIR")
|
|
|
|
// Check validity of the config
|
|
if err := validateConfig(cfg, logger); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go loadLogo(options.EmbeddedLogo, dataDir)
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (cfg *Config) GetServerAddr(df ...string) string {
|
|
return fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
|
}
|
|
|
|
func (cfg *Config) GetServerURI(df ...string) string {
|
|
pAddr := fmt.Sprintf("http://%s", cfg.GetServerAddr(df...))
|
|
if cfg.Server.Host == "" || cfg.Server.Host == "0.0.0.0" {
|
|
pAddr = fmt.Sprintf(":%d", cfg.Server.Port)
|
|
if len(df) > 0 {
|
|
pAddr = fmt.Sprintf("http://%s:%d", df[0], cfg.Server.Port)
|
|
}
|
|
}
|
|
return pAddr
|
|
}
|
|
|
|
func getWorkingDir(useBinaryPath bool) (string, error) {
|
|
// Get the working directory
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
binaryDir := ""
|
|
if exe, err := os.Executable(); err == nil {
|
|
if p, err := filepath.EvalSymlinks(exe); err == nil {
|
|
binaryDir = filepath.Dir(p)
|
|
binaryDir = filepath.FromSlash(binaryDir)
|
|
}
|
|
}
|
|
|
|
if useBinaryPath && binaryDir != "" {
|
|
return binaryDir, nil
|
|
}
|
|
|
|
//// Use the binary's directory as the working directory if needed
|
|
//if useBinaryPath {
|
|
// exe, err := os.Executable()
|
|
// if err != nil {
|
|
// return wd, nil // Fallback to working dir
|
|
// }
|
|
// p, err := filepath.EvalSymlinks(exe)
|
|
// if err != nil {
|
|
// return wd, nil // Fallback to working dir
|
|
// }
|
|
// wd = filepath.Dir(p) // Set the binary's directory as the working directory
|
|
// return wd, nil
|
|
//}
|
|
return wd, nil
|
|
}
|
|
|
|
func setDataDirEnv(dataDir string) error {
|
|
// Set the data directory environment variable
|
|
if os.Getenv("SEANIME_DATA_DIR") == "" {
|
|
if err := os.Setenv("SEANIME_DATA_DIR", dataDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setWorkingDirEnv(useBinaryPath bool) error {
|
|
// Set the working directory environment variable
|
|
wd, err := getWorkingDir(useBinaryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = os.Setenv("SEANIME_WORKING_DIR", filepath.FromSlash(wd)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateConfig checks if the config values are valid
|
|
func validateConfig(cfg *Config, logger *zerolog.Logger) error {
|
|
if cfg.Server.Host == "" {
|
|
return errInvalidConfigValue("server.host", "cannot be empty")
|
|
}
|
|
if cfg.Server.Port == 0 {
|
|
return errInvalidConfigValue("server.port", "cannot be 0")
|
|
}
|
|
if cfg.Database.Name == "" {
|
|
return errInvalidConfigValue("database.name", "cannot be empty")
|
|
}
|
|
if cfg.Web.AssetDir == "" {
|
|
return errInvalidConfigValue("web.assetDir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Web.AssetDir); err != nil {
|
|
return wrapInvalidConfigValue("web.assetDir", err)
|
|
}
|
|
|
|
if cfg.Cache.Dir == "" {
|
|
return errInvalidConfigValue("cache.dir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Cache.Dir); err != nil {
|
|
return wrapInvalidConfigValue("cache.dir", err)
|
|
}
|
|
|
|
if cfg.Cache.TranscodeDir == "" {
|
|
return errInvalidConfigValue("cache.transcodeDir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Cache.TranscodeDir); err != nil {
|
|
return wrapInvalidConfigValue("cache.transcodeDir", err)
|
|
}
|
|
|
|
if cfg.Logs.Dir == "" {
|
|
return errInvalidConfigValue("logs.dir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Logs.Dir); err != nil {
|
|
return wrapInvalidConfigValue("logs.dir", err)
|
|
}
|
|
|
|
if cfg.Manga.DownloadDir == "" {
|
|
return errInvalidConfigValue("manga.downloadDir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Manga.DownloadDir); err != nil {
|
|
return wrapInvalidConfigValue("manga.downloadDir", err)
|
|
}
|
|
|
|
if cfg.Manga.LocalDir == "" {
|
|
return errInvalidConfigValue("manga.localDir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Manga.LocalDir); err != nil {
|
|
return wrapInvalidConfigValue("manga.localDir", err)
|
|
}
|
|
|
|
if cfg.Extensions.Dir == "" {
|
|
return errInvalidConfigValue("extensions.dir", "cannot be empty")
|
|
}
|
|
if err := checkIsValidPath(cfg.Extensions.Dir); err != nil {
|
|
return wrapInvalidConfigValue("extensions.dir", err)
|
|
}
|
|
|
|
// Uncomment if "MainServerTorrentStreaming" is no longer an experimental feature
|
|
if cfg.Experimental.MainServerTorrentStreaming {
|
|
logger.Warn().Msgf("app: 'Main Server Torrent Streaming' feature is no longer experimental, remove the flag from your config file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkIsValidPath(path string) error {
|
|
ok := filepath.IsAbs(path)
|
|
if !ok {
|
|
return errors.New("path is not an absolute path")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// errInvalidConfigValue returns an error for an invalid config value
|
|
func errInvalidConfigValue(s string, s2 string) error {
|
|
return fmt.Errorf("invalid config value: \"%s\" %s", s, s2)
|
|
}
|
|
func wrapInvalidConfigValue(s string, err error) error {
|
|
return fmt.Errorf("invalid config value: \"%s\" %w", s, err)
|
|
}
|
|
|
|
func updateVersion(cfg *Config, opts *ConfigOptions) error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Do nothing
|
|
}
|
|
}()
|
|
|
|
if cfg.Version != constants.Version {
|
|
for _, f := range opts.OnVersionChange {
|
|
f(cfg.Version, constants.Version)
|
|
}
|
|
cfg.Version = constants.Version
|
|
}
|
|
|
|
viper.Set("version", constants.Version)
|
|
|
|
return viper.WriteConfig()
|
|
}
|
|
|
|
func expandEnvironmentValues(cfg *Config) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Do nothing
|
|
}
|
|
}()
|
|
cfg.Web.AssetDir = filepath.FromSlash(os.ExpandEnv(cfg.Web.AssetDir))
|
|
cfg.Cache.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Cache.Dir))
|
|
cfg.Cache.TranscodeDir = filepath.FromSlash(os.ExpandEnv(cfg.Cache.TranscodeDir))
|
|
cfg.Logs.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Logs.Dir))
|
|
cfg.Manga.DownloadDir = filepath.FromSlash(os.ExpandEnv(cfg.Manga.DownloadDir))
|
|
cfg.Manga.LocalDir = filepath.FromSlash(os.ExpandEnv(cfg.Manga.LocalDir))
|
|
cfg.Offline.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Offline.Dir))
|
|
cfg.Offline.AssetDir = filepath.FromSlash(os.ExpandEnv(cfg.Offline.AssetDir))
|
|
cfg.Extensions.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Extensions.Dir))
|
|
}
|
|
|
|
// createConfigFile creates a default config file if it doesn't exist
|
|
func createConfigFile(configPath string) error {
|
|
_, err := os.Stat(configPath)
|
|
if os.IsNotExist(err) {
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
|
|
return err
|
|
}
|
|
if err := viper.WriteConfig(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initAppDataDir(definedDataDir string, logger *zerolog.Logger) (dataDir string, configPath string, err error) {
|
|
|
|
// User defined data directory
|
|
if definedDataDir != "" {
|
|
|
|
// Expand environment variables
|
|
definedDataDir = filepath.FromSlash(os.ExpandEnv(definedDataDir))
|
|
|
|
if !filepath.IsAbs(definedDataDir) {
|
|
return "", "", errors.New("app: Data directory path must be absolute")
|
|
}
|
|
|
|
// Replace the default data directory
|
|
dataDir = definedDataDir
|
|
|
|
logger.Trace().Str("dataDir", dataDir).Msg("app: Overriding default data directory")
|
|
} else {
|
|
// Default OS data directory
|
|
// windows: %APPDATA%
|
|
// unix: $XDG_CONFIG_HOME or $HOME
|
|
// darwin: $HOME/Library/Application Support
|
|
dataDir, err = os.UserConfigDir()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
// Get the app directory
|
|
dataDir = filepath.Join(dataDir, "Seanime")
|
|
}
|
|
|
|
// Create data dir if it doesn't exist
|
|
if err := os.MkdirAll(dataDir, 0700); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Get the config file path
|
|
// Normalize the config file path
|
|
configPath = filepath.FromSlash(filepath.Join(dataDir, constants.ConfigFileName))
|
|
// Normalize the data directory path
|
|
dataDir = filepath.FromSlash(dataDir)
|
|
|
|
return
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func loadLogo(embeddedLogo []byte, dataDir string) (err error) {
|
|
defer util.HandlePanicInModuleWithError("core/loadLogo", &err)
|
|
|
|
if len(embeddedLogo) == 0 {
|
|
return nil
|
|
}
|
|
|
|
logoPath := filepath.Join(dataDir, "logo.png")
|
|
if _, err = os.Stat(logoPath); os.IsNotExist(err) {
|
|
if err = os.WriteFile(logoPath, embeddedLogo, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|