stash/internal/manager/config/config.go

1839 lines
47 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
// "github.com/sasha-s/go-deadlock" // if you have deadlock issues
"golang.org/x/crypto/bcrypt"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/stashapp/stash/internal/identify"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/hash"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/paths"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/utils"
)
const (
Stash = "stash"
Cache = "cache"
BackupDirectoryPath = "backup_directory_path"
Generated = "generated"
Metadata = "metadata"
BlobsPath = "blobs_path"
Downloads = "downloads"
ApiKey = "api_key"
Username = "username"
Password = "password"
MaxSessionAge = "max_session_age"
FFMpegPath = "ffmpeg_path"
FFProbePath = "ffprobe_path"
BlobsStorage = "blobs_storage"
DefaultMaxSessionAge = 60 * 60 * 1 // 1 hours
Database = "database"
Exclude = "exclude"
ImageExclude = "image_exclude"
VideoExtensions = "video_extensions"
ImageExtensions = "image_extensions"
GalleryExtensions = "gallery_extensions"
CreateGalleriesFromFolders = "create_galleries_from_folders"
// CalculateMD5 is the config key used to determine if MD5 should be calculated
// for video files.
CalculateMD5 = "calculate_md5"
// VideoFileNamingAlgorithm is the config key used to determine what hash
// should be used when generating and using generated files for scenes.
VideoFileNamingAlgorithm = "video_file_naming_algorithm"
MaxTranscodeSize = "max_transcode_size"
MaxStreamingTranscodeSize = "max_streaming_transcode_size"
// ffmpeg extra args options
TranscodeInputArgs = "ffmpeg.transcode.input_args"
TranscodeOutputArgs = "ffmpeg.transcode.output_args"
LiveTranscodeInputArgs = "ffmpeg.live_transcode.input_args"
LiveTranscodeOutputArgs = "ffmpeg.live_transcode.output_args"
ParallelTasks = "parallel_tasks"
parallelTasksDefault = 1
PreviewPreset = "preview_preset"
TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration"
SequentialScanning = "sequential_scanning"
SequentialScanningDefault = false
PreviewAudio = "preview_audio"
previewAudioDefault = true
PreviewSegmentDuration = "preview_segment_duration"
previewSegmentDurationDefault = 0.75
PreviewSegments = "preview_segments"
previewSegmentsDefault = 12
PreviewExcludeStart = "preview_exclude_start"
previewExcludeStartDefault = "0"
PreviewExcludeEnd = "preview_exclude_end"
previewExcludeEndDefault = "0"
WriteImageThumbnails = "write_image_thumbnails"
writeImageThumbnailsDefault = true
CreateImageClipsFromVideos = "create_image_clip_from_videos"
createImageClipsFromVideosDefault = false
Host = "host"
hostDefault = "0.0.0.0"
Port = "port"
portDefault = 9999
ExternalHost = "external_host"
// http proxy url if required
Proxy = "proxy"
// urls or IPs that should not use the proxy
NoProxy = "no_proxy"
noProxyDefault = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"
// key used to sign JWT tokens
JWTSignKey = "jwt_secret_key"
// key used for session store
SessionStoreKey = "session_store_key"
// scraping options
ScrapersPath = "scrapers_path"
ScraperUserAgent = "scraper_user_agent"
ScraperCertCheck = "scraper_cert_check"
ScraperCDPPath = "scraper_cdp_path"
ScraperExcludeTagPatterns = "scraper_exclude_tag_patterns"
// stash-box options
StashBoxes = "stash_boxes"
PythonPath = "python_path"
// plugin options
PluginsPath = "plugins_path"
PluginsSetting = "plugins.settings"
PluginsSettingPrefix = PluginsSetting + "."
DisabledPlugins = "plugins.disabled"
sourceDefaultPath = "community"
sourceDefaultName = "Community (stable)"
PluginPackageSources = "plugins.package_sources"
pluginPackageSourcesDefault = "https://stashapp.github.io/CommunityScripts/stable/index.yml"
ScraperPackageSources = "scrapers.package_sources"
scraperPackageSourcesDefault = "https://stashapp.github.io/CommunityScrapers/stable/index.yml"
// i18n
Language = "language"
// served directories
// this should be manually configured only
CustomServedFolders = "custom_served_folders"
// UI directory. Overrides to serve the UI from a specific location
// rather than use the embedded UI.
UILocation = "ui_location"
// backwards compatible name
LegacyCustomUILocation = "custom_ui_location"
// Gallery Cover Regex
GalleryCoverRegex = "gallery_cover_regex"
galleryCoverRegexDefault = `(poster|cover|folder|board)\.[^\.]+$`
// Interface options
MenuItems = "menu_items"
SoundOnPreview = "sound_on_preview"
WallShowTitle = "wall_show_title"
defaultWallShowTitle = true
CustomPerformerImageLocation = "custom_performer_image_location"
MaximumLoopDuration = "maximum_loop_duration"
AutostartVideo = "autostart_video"
AutostartVideoOnPlaySelected = "autostart_video_on_play_selected"
autostartVideoOnPlaySelectedDefault = true
ContinuePlaylistDefault = "continue_playlist_default"
ShowStudioAsText = "show_studio_as_text"
CSSEnabled = "cssenabled"
JavascriptEnabled = "javascriptenabled"
CustomLocalesEnabled = "customlocalesenabled"
ShowScrubber = "show_scrubber"
showScrubberDefault = true
WallPlayback = "wall_playback"
defaultWallPlayback = "video"
// Image lightbox options
legacyImageLightboxSlideshowDelay = "slideshow_delay"
ImageLightboxSlideshowDelay = "image_lightbox.slideshow_delay"
ImageLightboxDisplayModeKey = "image_lightbox.display_mode"
ImageLightboxScaleUp = "image_lightbox.scale_up"
ImageLightboxResetZoomOnNav = "image_lightbox.reset_zoom_on_nav"
ImageLightboxScrollModeKey = "image_lightbox.scroll_mode"
ImageLightboxScrollAttemptsBeforeChange = "image_lightbox.scroll_attempts_before_change"
UI = "ui"
defaultImageLightboxSlideshowDelay = 5
DisableDropdownCreatePerformer = "disable_dropdown_create.performer"
DisableDropdownCreateStudio = "disable_dropdown_create.studio"
DisableDropdownCreateTag = "disable_dropdown_create.tag"
DisableDropdownCreateMovie = "disable_dropdown_create.movie"
HandyKey = "handy_key"
FunscriptOffset = "funscript_offset"
UseStashHostedFunscript = "use_stash_hosted_funscript"
useStashHostedFunscriptDefault = false
DrawFunscriptHeatmapRange = "draw_funscript_heatmap_range"
drawFunscriptHeatmapRangeDefault = true
ThemeColor = "theme_color"
DefaultThemeColor = "#202b33"
// Security
dangerousAllowPublicWithoutAuth = "dangerous_allow_public_without_auth"
dangerousAllowPublicWithoutAuthDefault = "false"
SecurityTripwireAccessedFromPublicInternet = "security_tripwire_accessed_from_public_internet"
securityTripwireAccessedFromPublicInternetDefault = ""
// DLNA options
DLNAServerName = "dlna.server_name"
DLNADefaultEnabled = "dlna.default_enabled"
DLNADefaultIPWhitelist = "dlna.default_whitelist"
DLNAInterfaces = "dlna.interfaces"
DLNAVideoSortOrder = "dlna.video_sort_order"
dlnaVideoSortOrderDefault = "title"
DLNAPort = "dlna.port"
DLNAPortDefault = 1338
// Logging options
LogFile = "logfile"
LogOut = "logout"
defaultLogOut = true
LogLevel = "loglevel"
defaultLogLevel = "Info"
LogAccess = "logaccess"
defaultLogAccess = true
// Default settings
DefaultScanSettings = "defaults.scan_task"
DefaultIdentifySettings = "defaults.identify_task"
DefaultAutoTagSettings = "defaults.auto_tag_task"
DefaultGenerateSettings = "defaults.generate_task"
DeleteFileDefault = "defaults.delete_file"
DeleteGeneratedDefault = "defaults.delete_generated"
deleteGeneratedDefaultDefault = true
// Desktop Integration Options
NoBrowser = "nobrowser"
NoBrowserDefault = false
NotificationsEnabled = "notifications_enabled"
NotificationsEnabledDefault = true
ShowOneTimeMovedNotification = "show_one_time_moved_notification"
ShowOneTimeMovedNotificationDefault = false
// File upload options
MaxUploadSize = "max_upload_size"
// Developer options
ExtraBlobsPaths = "developer_options.extra_blob_paths"
)
// slice default values
var (
defaultVideoExtensions = []string{"m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm"}
defaultImageExtensions = []string{"png", "jpg", "jpeg", "gif", "webp"}
defaultGalleryExtensions = []string{"zip", "cbz"}
defaultMenuItems = []string{"scenes", "images", "movies", "markers", "galleries", "performers", "studios", "tags"}
)
var jsonUnmarshalConf = koanf.UnmarshalConf{
Tag: "json",
}
type MissingConfigError struct {
missingFields []string
}
func (e MissingConfigError) Error() string {
return fmt.Sprintf("missing the following mandatory settings: %s", strings.Join(e.missingFields, ", "))
}
// StashBoxError represents configuration errors of Stash-Box
type StashBoxError struct {
msg string
}
func (s *StashBoxError) Error() string {
// "Stash-box" is a proper noun and is therefore capitcalized
return "Stash-box: " + s.msg
}
type Config struct {
// main instance - backed by config file
main *koanf.Koanf
// override instance - populated from flags/environment
// not written to config file
overrides *koanf.Koanf
filePath string
isNewSystem bool
// configUpdates chan int
certFile string
keyFile string
sync.RWMutex
// deadlock.RWMutex // for deadlock testing/issues
}
var instance *Config
func GetInstance() *Config {
if instance == nil {
panic("config not initialized")
}
return instance
}
func (i *Config) load(f string) error {
if err := i.main.Load(file.Provider(f), yaml.Parser()); err != nil {
return err
}
i.filePath = f
return nil
}
func (i *Config) IsNewSystem() bool {
return i.isNewSystem
}
func (i *Config) SetConfigFile(fn string) {
i.Lock()
defer i.Unlock()
i.filePath = fn
}
func (i *Config) InitTLS() {
configDirectory := i.GetConfigPath()
tlsPaths := []string{
configDirectory,
paths.GetStashHomeDirectory(),
}
i.certFile = fsutil.FindInPaths(tlsPaths, "stash.crt")
i.keyFile = fsutil.FindInPaths(tlsPaths, "stash.key")
}
func (i *Config) GetTLSFiles() (certFile, keyFile string) {
return i.certFile, i.keyFile
}
func (i *Config) HasTLSConfig() bool {
certFile, keyFile := i.GetTLSFiles()
return certFile != "" && keyFile != ""
}
func (i *Config) GetNoBrowser() bool {
return i.getBool(NoBrowser)
}
func (i *Config) GetNotificationsEnabled() bool {
return i.getBool(NotificationsEnabled)
}
// GetShowOneTimeMovedNotification shows whether a small notification to inform the user that Stash
// will no longer show a terminal window, and instead will be available in the tray, should be shown.
// It is true when an existing system is started after upgrading, and set to false forever after it is shown.
func (i *Config) GetShowOneTimeMovedNotification() bool {
return i.getBool(ShowOneTimeMovedNotification)
}
// these methods are intended to ensure type safety (ie no primitive pointers)
func (i *Config) SetBool(key string, value bool) {
i.SetInterface(key, value)
}
func (i *Config) SetString(key string, value string) {
i.SetInterface(key, value)
}
func (i *Config) SetInt(key string, value int) {
i.SetInterface(key, value)
}
func (i *Config) SetFloat(key string, value float64) {
i.SetInterface(key, value)
}
func (i *Config) SetInterface(key string, value interface{}) {
i.Lock()
defer i.Unlock()
i.set(key, value)
}
func (i *Config) set(key string, value interface{}) {
// assumes lock held
// default behaviour for Set is to merge the value
// we want to replace it
i.main.Delete(key)
if value == nil {
return
}
// test for nil interface as well
refVal := reflect.ValueOf(value)
if refVal.Kind() == reflect.Ptr && refVal.IsNil() {
return
}
_ = i.main.Set(key, value)
}
func (i *Config) SetDefault(key string, value interface{}) {
i.Lock()
defer i.Unlock()
i.setDefault(key, value)
}
func (i *Config) setDefault(key string, value interface{}) {
if !i.main.Exists(key) {
i.set(key, value)
}
}
func (i *Config) SetPassword(value string) {
// if blank, don't bother hashing; we want it to be blank
if value == "" {
i.SetString(Password, "")
} else {
i.SetString(Password, hashPassword(value))
}
}
func (i *Config) Write() error {
i.Lock()
defer i.Unlock()
data, err := i.marshal()
if err != nil {
return err
}
return os.WriteFile(i.filePath, data, 0640)
}
func (i *Config) Marshal() ([]byte, error) {
i.RLock()
defer i.RUnlock()
return i.marshal()
}
func (i *Config) marshal() ([]byte, error) {
return i.main.Marshal(yaml.Parser())
}
// FileEnvSet returns true if the configuration file environment parameter
// is set.
func FileEnvSet() bool {
return os.Getenv("STASH_CONFIG_FILE") != ""
}
// GetConfigFile returns the full path to the used configuration file.
func (i *Config) GetConfigFile() string {
i.RLock()
defer i.RUnlock()
return i.filePath
}
// GetConfigPath returns the path of the directory containing the used
// configuration file.
func (i *Config) GetConfigPath() string {
return filepath.Dir(i.GetConfigFile())
}
// GetDefaultDatabaseFilePath returns the default database filename,
// which is located in the same directory as the config file.
func (i *Config) GetDefaultDatabaseFilePath() string {
return filepath.Join(i.GetConfigPath(), "stash-go.sqlite")
}
// forKey returns the Koanf instance that should be used to get the provided
// key. Returns the overrides instance if the key exists there, otherwise it
// returns the main instance. Assumes read lock held.
func (i *Config) forKey(key string) *koanf.Koanf {
v := i.main
if i.overrides.Exists(key) {
v = i.overrides
}
return v
}
// viper returns the viper instance that has the key set. Returns nil
// if no instance has the key. Assumes read lock held.
func (i *Config) with(key string) *koanf.Koanf {
v := i.forKey(key)
if v.Exists(key) {
return v
}
return nil
}
func (i *Config) HasOverride(key string) bool {
i.RLock()
defer i.RUnlock()
return i.overrides.Exists(key)
}
// These functions wrap the equivalent viper functions, checking the override
// instance first, then the main instance.
func (i *Config) unmarshalKey(key string, rawVal interface{}) error {
i.RLock()
defer i.RUnlock()
return i.forKey(key).Unmarshal(key, rawVal)
}
func (i *Config) getStringSlice(key string) []string {
i.RLock()
defer i.RUnlock()
return i.forKey(key).Strings(key)
}
func (i *Config) getString(key string) string {
i.RLock()
defer i.RUnlock()
return i.forKey(key).String(key)
}
func (i *Config) getBool(key string) bool {
i.RLock()
defer i.RUnlock()
return i.forKey(key).Bool(key)
}
func (i *Config) getBoolDefault(key string, def bool) bool {
i.RLock()
defer i.RUnlock()
ret := def
v := i.forKey(key)
if v.Exists(key) {
ret = v.Bool(key)
}
return ret
}
func (i *Config) getInt(key string) int {
i.RLock()
defer i.RUnlock()
return i.forKey(key).Int(key)
}
func (i *Config) getFloat64(key string) float64 {
i.RLock()
defer i.RUnlock()
return i.forKey(key).Float64(key)
}
func (i *Config) getStringMapString(key string) map[string]string {
i.RLock()
defer i.RUnlock()
ret := i.forKey(key).StringMap(key)
// GetStringMapString returns an empty map regardless of whether the
// key exists or not.
if len(ret) == 0 {
return nil
}
return ret
}
// GetStathPaths returns the configured stash library paths.
// Works opposite to the usual case - it will return the override
// value only if the main value is not set.
func (i *Config) GetStashPaths() StashConfigs {
i.RLock()
defer i.RUnlock()
var ret StashConfigs
v := i.main
if !v.Exists(Stash) {
v = i.overrides
}
if err := v.Unmarshal(Stash, &ret); err != nil || len(ret) == 0 {
// fallback to legacy format
ss := v.Strings(Stash)
ret = nil
for _, path := range ss {
toAdd := &StashConfig{
Path: path,
}
ret = append(ret, toAdd)
}
}
return ret
}
func (i *Config) GetCachePath() string {
return i.getString(Cache)
}
func (i *Config) GetGeneratedPath() string {
return i.getString(Generated)
}
func (i *Config) GetBlobsPath() string {
return i.getString(BlobsPath)
}
// GetExtraBlobsPaths returns extra blobs paths.
// For developer/advanced use only.
func (i *Config) GetExtraBlobsPaths() []string {
return i.getStringSlice(ExtraBlobsPaths)
}
func (i *Config) GetBlobsStorage() BlobsStorageType {
ret := BlobsStorageType(i.getString(BlobsStorage))
if !ret.IsValid() {
// default to database storage
// for legacy systems this is probably the safer option
ret = BlobStorageTypeDatabase
}
return ret
}
func (i *Config) GetMetadataPath() string {
return i.getString(Metadata)
}
func (i *Config) GetDatabasePath() string {
return i.getString(Database)
}
func (i *Config) GetBackupDirectoryPath() string {
return i.getString(BackupDirectoryPath)
}
func (i *Config) GetBackupDirectoryPathOrDefault() string {
ret := i.GetBackupDirectoryPath()
if ret == "" {
return i.GetConfigPath()
}
return ret
}
// GetFFMpegPath returns the path to the FFMpeg executable.
// If empty, stash will attempt to resolve it from the path.
func (i *Config) GetFFMpegPath() string {
return i.getString(FFMpegPath)
}
// GetFFProbePath returns the path to the FFProbe executable.
// If empty, stash will attempt to resolve it from the path.
func (i *Config) GetFFProbePath() string {
return i.getString(FFProbePath)
}
func (i *Config) GetJWTSignKey() []byte {
return []byte(i.getString(JWTSignKey))
}
func (i *Config) GetSessionStoreKey() []byte {
return []byte(i.getString(SessionStoreKey))
}
func (i *Config) GetDefaultScrapersPath() string {
// default to the same directory as the config file
fn := filepath.Join(i.GetConfigPath(), "scrapers")
return fn
}
func (i *Config) GetExcludes() []string {
return i.getStringSlice(Exclude)
}
func (i *Config) GetImageExcludes() []string {
return i.getStringSlice(ImageExclude)
}
func (i *Config) GetVideoExtensions() []string {
ret := i.getStringSlice(VideoExtensions)
if len(ret) == 0 {
ret = defaultVideoExtensions
}
return ret
}
func (i *Config) GetImageExtensions() []string {
ret := i.getStringSlice(ImageExtensions)
if len(ret) == 0 {
ret = defaultImageExtensions
}
return ret
}
func (i *Config) GetGalleryExtensions() []string {
ret := i.getStringSlice(GalleryExtensions)
if len(ret) == 0 {
ret = defaultGalleryExtensions
}
return ret
}
func (i *Config) GetCreateGalleriesFromFolders() bool {
return i.getBool(CreateGalleriesFromFolders)
}
func (i *Config) GetLanguage() string {
ret := i.getString(Language)
// default to English
if ret == "" {
return "en-US"
}
return ret
}
// IsCalculateMD5 returns true if MD5 checksums should be generated for
// scene video files.
func (i *Config) IsCalculateMD5() bool {
return i.getBool(CalculateMD5)
}
// GetVideoFileNamingAlgorithm returns what hash algorithm should be used for
// naming generated scene video files.
func (i *Config) GetVideoFileNamingAlgorithm() models.HashAlgorithm {
ret := i.getString(VideoFileNamingAlgorithm)
// default to oshash
if ret == "" {
return models.HashAlgorithmOshash
}
return models.HashAlgorithm(ret)
}
func (i *Config) GetSequentialScanning() bool {
return i.getBool(SequentialScanning)
}
func (i *Config) GetGalleryCoverRegex() string {
var regexString = i.getString(GalleryCoverRegex)
_, err := regexp.Compile(regexString)
if err != nil {
logger.Warnf("Gallery cover regex '%v' invalid, reverting to default.", regexString)
return galleryCoverRegexDefault
}
return regexString
}
func (i *Config) GetScrapersPath() string {
return i.getString(ScrapersPath)
}
func (i *Config) GetScraperUserAgent() string {
return i.getString(ScraperUserAgent)
}
// GetScraperCDPPath gets the path to the Chrome executable or remote address
// to an instance of Chrome.
func (i *Config) GetScraperCDPPath() string {
return i.getString(ScraperCDPPath)
}
// GetScraperCertCheck returns true if the scraper should check for insecure
// certificates when fetching an image or a page.
func (i *Config) GetScraperCertCheck() bool {
return i.getBoolDefault(ScraperCertCheck, true)
}
func (i *Config) GetScraperExcludeTagPatterns() []string {
return i.getStringSlice(ScraperExcludeTagPatterns)
}
func (i *Config) GetStashBoxes() []*models.StashBox {
var boxes []*models.StashBox
if err := i.unmarshalKey(StashBoxes, &boxes); err != nil {
logger.Warnf("error in unmarshalkey: %v", err)
}
return boxes
}
func (i *Config) GetDefaultPluginsPath() string {
// default to the same directory as the config file
fn := filepath.Join(i.GetConfigPath(), "plugins")
return fn
}
func (i *Config) GetPluginsPath() string {
return i.getString(PluginsPath)
}
func (i *Config) GetAllPluginConfiguration() map[string]map[string]interface{} {
i.RLock()
defer i.RUnlock()
ret := make(map[string]map[string]interface{})
v := i.forKey(PluginsSetting)
sub := v.Cut(PluginsSetting)
if sub == nil {
return ret
}
for plugin := range sub.Raw() {
ret[plugin] = sub.Cut(plugin).Raw()
}
return ret
}
func (i *Config) GetPluginConfiguration(pluginID string) map[string]interface{} {
i.RLock()
defer i.RUnlock()
key := PluginsSettingPrefix + pluginID
return i.forKey(key).Cut(key).Raw()
}
// SetPluginConfiguration sets the configuration for a plugin.
// It will overwrite any existing configuration.
func (i *Config) SetPluginConfiguration(pluginID string, v map[string]interface{}) {
i.Lock()
defer i.Unlock()
key := PluginsSettingPrefix + pluginID
i.set(key, v)
}
func (i *Config) GetDisabledPlugins() []string {
return i.getStringSlice(DisabledPlugins)
}
func (i *Config) GetPythonPath() string {
return i.getString(PythonPath)
}
func (i *Config) GetHost() string {
ret := i.getString(Host)
if ret == "" {
ret = hostDefault
}
return ret
}
func (i *Config) GetPort() int {
ret := i.getInt(Port)
if ret == 0 {
ret = portDefault
}
return ret
}
func (i *Config) GetThemeColor() string {
return i.getString(ThemeColor)
}
func (i *Config) GetExternalHost() string {
return i.getString(ExternalHost)
}
// GetPreviewSegmentDuration returns the duration of a single segment in a
// scene preview file, in seconds.
func (i *Config) GetPreviewSegmentDuration() float64 {
return i.getFloat64(PreviewSegmentDuration)
}
// GetParallelTasks returns the number of parallel tasks that should be started
// by scan or generate task.
func (i *Config) GetParallelTasks() int {
return i.getInt(ParallelTasks)
}
func (i *Config) GetParallelTasksWithAutoDetection() int {
parallelTasks := i.getInt(ParallelTasks)
if parallelTasks <= 0 {
parallelTasks = (runtime.NumCPU() / 4) + 1
}
return parallelTasks
}
func (i *Config) GetPreviewAudio() bool {
return i.getBool(PreviewAudio)
}
// GetPreviewSegments returns the amount of segments in a scene preview file.
func (i *Config) GetPreviewSegments() int {
return i.getInt(PreviewSegments)
}
// GetPreviewExcludeStart returns the configuration setting string for
// excluding the start of scene videos for preview generation. This can
// be in two possible formats. A float value is interpreted as the amount
// of seconds to exclude from the start of the video before it is included
// in the preview. If the value is suffixed with a '%' character (for example
// '2%'), then it is interpreted as a proportion of the total video duration.
func (i *Config) GetPreviewExcludeStart() string {
return i.getString(PreviewExcludeStart)
}
// GetPreviewExcludeEnd returns the configuration setting string for
// excluding the end of scene videos for preview generation. A float value
// is interpreted as the amount of seconds to exclude from the end of the video
// when generating previews. If the value is suffixed with a '%' character,
// then it is interpreted as a proportion of the total video duration.
func (i *Config) GetPreviewExcludeEnd() string {
return i.getString(PreviewExcludeEnd)
}
// GetPreviewPreset returns the preset when generating previews. Defaults to
// Slow.
func (i *Config) GetPreviewPreset() models.PreviewPreset {
ret := i.getString(PreviewPreset)
// default to slow
if ret == "" {
return models.PreviewPresetSlow
}
return models.PreviewPreset(ret)
}
func (i *Config) GetTranscodeHardwareAcceleration() bool {
return i.getBool(TranscodeHardwareAcceleration)
}
func (i *Config) GetMaxTranscodeSize() models.StreamingResolutionEnum {
ret := i.getString(MaxTranscodeSize)
// default to original
if ret == "" {
return models.StreamingResolutionEnumOriginal
}
return models.StreamingResolutionEnum(ret)
}
func (i *Config) GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum {
ret := i.getString(MaxStreamingTranscodeSize)
// default to original
if ret == "" {
return models.StreamingResolutionEnumOriginal
}
return models.StreamingResolutionEnum(ret)
}
func (i *Config) GetTranscodeInputArgs() []string {
return i.getStringSlice(TranscodeInputArgs)
}
func (i *Config) GetTranscodeOutputArgs() []string {
return i.getStringSlice(TranscodeOutputArgs)
}
func (i *Config) GetLiveTranscodeInputArgs() []string {
return i.getStringSlice(LiveTranscodeInputArgs)
}
func (i *Config) GetLiveTranscodeOutputArgs() []string {
return i.getStringSlice(LiveTranscodeOutputArgs)
}
func (i *Config) GetDrawFunscriptHeatmapRange() bool {
return i.getBoolDefault(DrawFunscriptHeatmapRange, drawFunscriptHeatmapRangeDefault)
}
// IsWriteImageThumbnails returns true if image thumbnails should be written
// to disk after generating on the fly.
func (i *Config) IsWriteImageThumbnails() bool {
return i.getBool(WriteImageThumbnails)
}
func (i *Config) IsCreateImageClipsFromVideos() bool {
return i.getBool(CreateImageClipsFromVideos)
}
func (i *Config) GetAPIKey() string {
return i.getString(ApiKey)
}
func (i *Config) GetUsername() string {
return i.getString(Username)
}
func (i *Config) GetPasswordHash() string {
return i.getString(Password)
}
func (i *Config) GetCredentials() (string, string) {
if i.HasCredentials() {
return i.getString(Username), i.getString(Password)
}
return "", ""
}
func (i *Config) HasCredentials() bool {
username := i.getString(Username)
pwHash := i.getString(Password)
return username != "" && pwHash != ""
}
func hashPassword(password string) string {
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
return string(hash)
}
func (i *Config) ValidateCredentials(username string, password string) bool {
if !i.HasCredentials() {
// don't need to authenticate if no credentials saved
return true
}
authUser, authPWHash := i.GetCredentials()
err := bcrypt.CompareHashAndPassword([]byte(authPWHash), []byte(password))
return username == authUser && err == nil
}
var stashBoxRe = regexp.MustCompile("^http.*graphql$")
type StashBoxInput struct {
Endpoint string `json:"endpoint"`
APIKey string `json:"api_key"`
Name string `json:"name"`
}
func (i *Config) ValidateStashBoxes(boxes []*StashBoxInput) error {
isMulti := len(boxes) > 1
for _, box := range boxes {
// Validate each stash-box configuration field, return on error
if box.APIKey == "" {
return &StashBoxError{msg: "API Key cannot be blank"}
}
if box.Endpoint == "" {
return &StashBoxError{msg: "endpoint cannot be blank"}
}
if !stashBoxRe.Match([]byte(box.Endpoint)) {
return &StashBoxError{msg: "endpoint is invalid"}
}
if isMulti && box.Name == "" {
return &StashBoxError{msg: "name cannot be blank"}
}
}
return nil
}
// GetMaxSessionAge gets the maximum age for session cookies, in seconds.
// Session cookie expiry times are refreshed every request.
func (i *Config) GetMaxSessionAge() int {
i.RLock()
defer i.RUnlock()
ret := DefaultMaxSessionAge
v := i.forKey(MaxSessionAge)
if v.Exists(MaxSessionAge) {
ret = v.Int(MaxSessionAge)
}
return ret
}
// GetCustomServedFolders gets the map of custom paths to their applicable
// filesystem locations
func (i *Config) GetCustomServedFolders() utils.URLMap {
return i.getStringMapString(CustomServedFolders)
}
func (i *Config) GetUILocation() string {
if ret := i.getString(UILocation); ret != "" {
return ret
}
return i.getString(LegacyCustomUILocation)
}
// Interface options
func (i *Config) GetMenuItems() []string {
i.RLock()
defer i.RUnlock()
v := i.forKey(MenuItems)
if v.Exists(MenuItems) {
return v.Strings(MenuItems)
}
return defaultMenuItems
}
func (i *Config) GetSoundOnPreview() bool {
return i.getBool(SoundOnPreview)
}
func (i *Config) GetWallShowTitle() bool {
i.RLock()
defer i.RUnlock()
ret := defaultWallShowTitle
v := i.forKey(WallShowTitle)
if v.Exists(WallShowTitle) {
ret = v.Bool(WallShowTitle)
}
return ret
}
func (i *Config) GetCustomPerformerImageLocation() string {
return i.getString(CustomPerformerImageLocation)
}
func (i *Config) GetWallPlayback() string {
i.RLock()
defer i.RUnlock()
ret := defaultWallPlayback
v := i.forKey(WallPlayback)
if v.Exists(WallPlayback) {
ret = v.String(WallPlayback)
}
return ret
}
func (i *Config) GetShowScrubber() bool {
return i.getBoolDefault(ShowScrubber, showScrubberDefault)
}
func (i *Config) GetMaximumLoopDuration() int {
return i.getInt(MaximumLoopDuration)
}
func (i *Config) GetAutostartVideo() bool {
return i.getBool(AutostartVideo)
}
func (i *Config) GetAutostartVideoOnPlaySelected() bool {
return i.getBoolDefault(AutostartVideoOnPlaySelected, autostartVideoOnPlaySelectedDefault)
}
func (i *Config) GetContinuePlaylistDefault() bool {
return i.getBool(ContinuePlaylistDefault)
}
func (i *Config) GetShowStudioAsText() bool {
return i.getBool(ShowStudioAsText)
}
func (i *Config) getSlideshowDelay() int {
// assume have lock
ret := defaultImageLightboxSlideshowDelay
v := i.forKey(ImageLightboxSlideshowDelay)
if v.Exists(ImageLightboxSlideshowDelay) {
ret = v.Int(ImageLightboxSlideshowDelay)
} else {
// fallback to old location
v := i.forKey(legacyImageLightboxSlideshowDelay)
if v.Exists(legacyImageLightboxSlideshowDelay) {
ret = v.Int(legacyImageLightboxSlideshowDelay)
}
}
return ret
}
func (i *Config) GetImageLightboxOptions() ConfigImageLightboxResult {
i.RLock()
defer i.RUnlock()
delay := i.getSlideshowDelay()
ret := ConfigImageLightboxResult{
SlideshowDelay: &delay,
}
if v := i.with(ImageLightboxDisplayModeKey); v != nil {
mode := ImageLightboxDisplayMode(v.String(ImageLightboxDisplayModeKey))
ret.DisplayMode = &mode
}
if v := i.with(ImageLightboxScaleUp); v != nil {
value := v.Bool(ImageLightboxScaleUp)
ret.ScaleUp = &value
}
if v := i.with(ImageLightboxResetZoomOnNav); v != nil {
value := v.Bool(ImageLightboxResetZoomOnNav)
ret.ResetZoomOnNav = &value
}
if v := i.with(ImageLightboxScrollModeKey); v != nil {
mode := ImageLightboxScrollMode(v.String(ImageLightboxScrollModeKey))
ret.ScrollMode = &mode
}
if v := i.with(ImageLightboxScrollAttemptsBeforeChange); v != nil {
ret.ScrollAttemptsBeforeChange = v.Int(ImageLightboxScrollAttemptsBeforeChange)
}
return ret
}
func (i *Config) GetDisableDropdownCreate() *ConfigDisableDropdownCreate {
return &ConfigDisableDropdownCreate{
Performer: i.getBool(DisableDropdownCreatePerformer),
Studio: i.getBool(DisableDropdownCreateStudio),
Tag: i.getBool(DisableDropdownCreateTag),
Movie: i.getBool(DisableDropdownCreateMovie),
}
}
func (i *Config) GetUIConfiguration() map[string]interface{} {
i.RLock()
defer i.RUnlock()
return i.forKey(UI).Cut(UI).Raw()
}
func (i *Config) SetUIConfiguration(v map[string]interface{}) {
i.Lock()
defer i.Unlock()
i.set(UI, v)
}
func (i *Config) GetCSSPath() string {
// use custom.css in the same directory as the config file
configFileUsed := i.GetConfigFile()
configDir := filepath.Dir(configFileUsed)
fn := filepath.Join(configDir, "custom.css")
return fn
}
func (i *Config) GetCSS() string {
fn := i.GetCSSPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return ""
}
buf, err := os.ReadFile(fn)
if err != nil {
return ""
}
return string(buf)
}
func (i *Config) SetCSS(css string) {
fn := i.GetCSSPath()
i.Lock()
defer i.Unlock()
buf := []byte(css)
if err := os.WriteFile(fn, buf, 0777); err != nil {
logger.Warnf("error while writing %v bytes to %v: %v", len(buf), fn, err)
}
}
func (i *Config) GetCSSEnabled() bool {
return i.getBool(CSSEnabled)
}
func (i *Config) GetJavascriptPath() string {
// use custom.js in the same directory as the config file
configFileUsed := i.GetConfigFile()
configDir := filepath.Dir(configFileUsed)
fn := filepath.Join(configDir, "custom.js")
return fn
}
func (i *Config) GetJavascript() string {
fn := i.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return ""
}
buf, err := os.ReadFile(fn)
if err != nil {
return ""
}
return string(buf)
}
func (i *Config) SetJavascript(javascript string) {
fn := i.GetJavascriptPath()
i.Lock()
defer i.Unlock()
buf := []byte(javascript)
if err := os.WriteFile(fn, buf, 0777); err != nil {
logger.Warnf("error while writing %v bytes to %v: %v", len(buf), fn, err)
}
}
func (i *Config) GetJavascriptEnabled() bool {
return i.getBool(JavascriptEnabled)
}
func (i *Config) GetCustomLocalesPath() string {
// use custom-locales.json in the same directory as the config file
configFileUsed := i.GetConfigFile()
configDir := filepath.Dir(configFileUsed)
fn := filepath.Join(configDir, "custom-locales.json")
return fn
}
func (i *Config) GetCustomLocales() string {
fn := i.GetCustomLocalesPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return ""
}
buf, err := os.ReadFile(fn)
if err != nil {
return ""
}
return string(buf)
}
func (i *Config) SetCustomLocales(customLocales string) {
fn := i.GetCustomLocalesPath()
i.Lock()
defer i.Unlock()
buf := []byte(customLocales)
if err := os.WriteFile(fn, buf, 0777); err != nil {
logger.Warnf("error while writing %v bytes to %v: %v", len(buf), fn, err)
}
}
func (i *Config) GetCustomLocalesEnabled() bool {
return i.getBool(CustomLocalesEnabled)
}
func (i *Config) GetHandyKey() string {
return i.getString(HandyKey)
}
func (i *Config) GetFunscriptOffset() int {
return i.getInt(FunscriptOffset)
}
func (i *Config) GetUseStashHostedFunscript() bool {
return i.getBoolDefault(UseStashHostedFunscript, useStashHostedFunscriptDefault)
}
func (i *Config) GetDeleteFileDefault() bool {
return i.getBool(DeleteFileDefault)
}
func (i *Config) GetDeleteGeneratedDefault() bool {
return i.getBoolDefault(DeleteGeneratedDefault, deleteGeneratedDefaultDefault)
}
// GetDefaultIdentifySettings returns the default Identify task settings.
// Returns nil if the settings could not be unmarshalled, or if it
// has not been set.
func (i *Config) GetDefaultIdentifySettings() *identify.Options {
i.RLock()
defer i.RUnlock()
v := i.forKey(DefaultIdentifySettings)
if v.Exists(DefaultIdentifySettings) && v.Get(DefaultIdentifySettings) != nil {
var ret identify.Options
if err := v.UnmarshalWithConf(DefaultIdentifySettings, &ret, jsonUnmarshalConf); err != nil {
return nil
}
return &ret
}
return nil
}
// GetDefaultScanSettings returns the default Scan task settings.
// Returns nil if the settings could not be unmarshalled, or if it
// has not been set.
func (i *Config) GetDefaultScanSettings() *ScanMetadataOptions {
i.RLock()
defer i.RUnlock()
v := i.forKey(DefaultScanSettings)
if v.Exists(DefaultScanSettings) && v.Get(DefaultScanSettings) != nil {
var ret ScanMetadataOptions
if err := v.UnmarshalWithConf(DefaultScanSettings, &ret, jsonUnmarshalConf); err != nil {
return nil
}
return &ret
}
return nil
}
// GetDefaultAutoTagSettings returns the default Scan task settings.
// Returns nil if the settings could not be unmarshalled, or if it
// has not been set.
func (i *Config) GetDefaultAutoTagSettings() *AutoTagMetadataOptions {
i.RLock()
defer i.RUnlock()
v := i.forKey(DefaultAutoTagSettings)
if v.Exists(DefaultAutoTagSettings) {
var ret AutoTagMetadataOptions
if err := v.Unmarshal(DefaultAutoTagSettings, &ret); err != nil {
return nil
}
return &ret
}
return nil
}
// GetDefaultGenerateSettings returns the default Scan task settings.
// Returns nil if the settings could not be unmarshalled, or if it
// has not been set.
func (i *Config) GetDefaultGenerateSettings() *models.GenerateMetadataOptions {
i.RLock()
defer i.RUnlock()
v := i.forKey(DefaultGenerateSettings)
if v.Exists(DefaultGenerateSettings) {
var ret models.GenerateMetadataOptions
if err := v.Unmarshal(DefaultGenerateSettings, &ret); err != nil {
return nil
}
return &ret
}
return nil
}
// GetDangerousAllowPublicWithoutAuth determines if the security feature is enabled.
// See https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet
func (i *Config) GetDangerousAllowPublicWithoutAuth() bool {
return i.getBool(dangerousAllowPublicWithoutAuth)
}
// GetSecurityTripwireAccessedFromPublicInternet returns a public IP address if stash
// has been accessed from the public internet, with no auth enabled, and
// DangerousAllowPublicWithoutAuth disabled. Returns an empty string otherwise.
func (i *Config) GetSecurityTripwireAccessedFromPublicInternet() string {
return i.getString(SecurityTripwireAccessedFromPublicInternet)
}
// GetDLNAServerName returns the visible name of the DLNA server. If empty,
// "stash" will be used.
func (i *Config) GetDLNAServerName() string {
return i.getString(DLNAServerName)
}
// GetDLNADefaultEnabled returns true if the DLNA is enabled by default.
func (i *Config) GetDLNADefaultEnabled() bool {
return i.getBool(DLNADefaultEnabled)
}
// GetDLNADefaultIPWhitelist returns a list of IP addresses/wildcards that
// are allowed to use the DLNA service.
func (i *Config) GetDLNADefaultIPWhitelist() []string {
return i.getStringSlice(DLNADefaultIPWhitelist)
}
// GetDLNAInterfaces returns a list of interface names to expose DLNA on. If
// empty, runs on all interfaces.
func (i *Config) GetDLNAInterfaces() []string {
return i.getStringSlice(DLNAInterfaces)
}
// GetDLNAPort returns the port to run the DLNA server on. If empty, 1338
// will be used.
func (i *Config) GetDLNAPort() int {
ret := i.getInt(DLNAPort)
if ret == 0 {
ret = DLNAPortDefault
}
return ret
}
// GetDLNAPortAsString returns the port to run the DLNA server on as a string.
func (i *Config) GetDLNAPortAsString() string {
return ":" + strconv.Itoa(i.GetDLNAPort())
}
// GetVideoSortOrder returns the sort order to display videos. If
// empty, videos will be sorted by titles.
func (i *Config) GetVideoSortOrder() string {
ret := i.getString(DLNAVideoSortOrder)
if ret == "" {
ret = dlnaVideoSortOrderDefault
}
return ret
}
// GetLogFile returns the filename of the file to output logs to.
// An empty string means that file logging will be disabled.
func (i *Config) GetLogFile() string {
return i.getString(LogFile)
}
// GetLogOut returns true if logging should be output to the terminal
// in addition to writing to a log file. Logging will be output to the
// terminal if file logging is disabled. Defaults to true.
func (i *Config) GetLogOut() bool {
return i.getBoolDefault(LogOut, defaultLogOut)
}
// GetLogLevel returns the lowest log level to write to the log.
// Should be one of "Debug", "Info", "Warning", "Error"
func (i *Config) GetLogLevel() string {
value := i.getString(LogLevel)
if value != "Debug" && value != "Info" && value != "Warning" && value != "Error" && value != "Trace" {
value = defaultLogLevel
}
return value
}
// GetLogAccess returns true if http requests should be logged to the terminal.
// HTTP requests are not logged to the log file. Defaults to true.
func (i *Config) GetLogAccess() bool {
return i.getBoolDefault(LogAccess, defaultLogAccess)
}
// Max allowed graphql upload size in megabytes
func (i *Config) GetMaxUploadSize() int64 {
i.RLock()
defer i.RUnlock()
ret := int64(1024)
v := i.forKey(MaxUploadSize)
if v.Exists(MaxUploadSize) {
ret = v.Int64(MaxUploadSize)
}
return ret << 20
}
// GetProxy returns the url of a http proxy to be used for all outgoing http calls.
func (i *Config) GetProxy() string {
// Validate format
reg := regexp.MustCompile(`^((?:socks5h?|https?):\/\/)(([\P{Cc}]+):([\P{Cc}]+)@)?(([a-zA-Z0-9][a-zA-Z0-9.-]*)(:[0-9]{1,5})?)`)
proxy := i.getString(Proxy)
if proxy != "" && reg.MatchString(proxy) {
logger.Debug("Proxy is valid, using it")
return proxy
} else if proxy != "" {
logger.Error("Proxy is invalid, please review your configuration")
return ""
}
return ""
}
// GetProxy returns the url of a http proxy to be used for all outgoing http calls.
func (i *Config) GetNoProxy() string {
// NoProxy does not require validation, it is validated by the native Go library sufficiently
return i.getString(NoProxy)
}
// ActivatePublicAccessTripwire sets the security_tripwire_accessed_from_public_internet
// config field to the provided IP address to indicate that stash has been accessed
// from this public IP without authentication.
func (i *Config) ActivatePublicAccessTripwire(requestIP string) error {
i.SetString(SecurityTripwireAccessedFromPublicInternet, requestIP)
return i.Write()
}
func (i *Config) getPackageSources(key string) []*models.PackageSource {
var sources []*models.PackageSource
if err := i.unmarshalKey(key, &sources); err != nil {
logger.Warnf("error in unmarshalkey: %v", err)
}
return sources
}
func (i *Config) GetPluginPackageSources() []*models.PackageSource {
return i.getPackageSources(PluginPackageSources)
}
func (i *Config) GetScraperPackageSources() []*models.PackageSource {
return i.getPackageSources(ScraperPackageSources)
}
type packagePathGetter struct {
getterFn func() []*models.PackageSource
}
func (g packagePathGetter) GetAllSourcePaths() []string {
p := g.getterFn()
var ret []string
for _, v := range p {
ret = sliceutil.AppendUnique(ret, v.LocalPath)
}
return ret
}
func (g packagePathGetter) GetSourcePath(srcURL string) string {
p := g.getterFn()
for _, v := range p {
if v.URL == srcURL {
return v.LocalPath
}
}
return ""
}
func (i *Config) GetPluginPackagePathGetter() packagePathGetter {
return packagePathGetter{
getterFn: i.GetPluginPackageSources,
}
}
func (i *Config) GetScraperPackagePathGetter() packagePathGetter {
return packagePathGetter{
getterFn: i.GetScraperPackageSources,
}
}
func (i *Config) Validate() error {
i.RLock()
defer i.RUnlock()
mandatoryPaths := []string{
Database,
Generated,
}
var missingFields []string
for _, p := range mandatoryPaths {
if !i.forKey(p).Exists(p) || i.forKey(p).String(p) == "" {
missingFields = append(missingFields, p)
}
}
if len(missingFields) > 0 {
return MissingConfigError{
missingFields: missingFields,
}
}
if i.GetBlobsStorage() == BlobStorageTypeFilesystem && i.forKey(BlobsPath).String(BlobsPath) == "" {
return MissingConfigError{
missingFields: []string{BlobsPath},
}
}
return nil
}
func (i *Config) setDefaultValues() {
// read data before write lock scope
defaultDatabaseFilePath := i.GetDefaultDatabaseFilePath()
defaultScrapersPath := i.GetDefaultScrapersPath()
defaultPluginsPath := i.GetDefaultPluginsPath()
i.Lock()
defer i.Unlock()
// set the default host and port so that these are written to the config
// file
i.setDefault(Host, hostDefault)
i.setDefault(Port, portDefault)
i.setDefault(ParallelTasks, parallelTasksDefault)
i.setDefault(SequentialScanning, SequentialScanningDefault)
i.setDefault(PreviewSegmentDuration, previewSegmentDurationDefault)
i.setDefault(PreviewSegments, previewSegmentsDefault)
i.setDefault(PreviewExcludeStart, previewExcludeStartDefault)
i.setDefault(PreviewExcludeEnd, previewExcludeEndDefault)
i.setDefault(PreviewAudio, previewAudioDefault)
i.setDefault(SoundOnPreview, false)
i.setDefault(ThemeColor, DefaultThemeColor)
i.setDefault(WriteImageThumbnails, writeImageThumbnailsDefault)
i.setDefault(CreateImageClipsFromVideos, createImageClipsFromVideosDefault)
i.setDefault(Database, defaultDatabaseFilePath)
i.setDefault(dangerousAllowPublicWithoutAuth, dangerousAllowPublicWithoutAuthDefault)
i.setDefault(SecurityTripwireAccessedFromPublicInternet, securityTripwireAccessedFromPublicInternetDefault)
// Set generated to the metadata path for backwards compat
i.setDefault(Generated, i.main.String(Metadata))
i.setDefault(NoBrowser, NoBrowserDefault)
i.setDefault(NotificationsEnabled, NotificationsEnabledDefault)
i.setDefault(ShowOneTimeMovedNotification, ShowOneTimeMovedNotificationDefault)
// Set default scrapers and plugins paths
i.setDefault(ScrapersPath, defaultScrapersPath)
i.setDefault(PluginsPath, defaultPluginsPath)
// Set default gallery cover regex
i.setDefault(GalleryCoverRegex, galleryCoverRegexDefault)
// Set NoProxy default
i.setDefault(NoProxy, noProxyDefault)
// set default package sources
i.setDefault(PluginPackageSources, []map[string]string{{
"name": sourceDefaultName,
"url": pluginPackageSourcesDefault,
"localpath": sourceDefaultPath,
}})
i.setDefault(ScraperPackageSources, []map[string]string{{
"name": sourceDefaultName,
"url": scraperPackageSourcesDefault,
"localpath": sourceDefaultPath,
}})
}
// setExistingSystemDefaults sets config options that are new and unset in an existing install,
// but should have a separate default than for brand-new systems, to maintain behavior.
// The config file will not be written.
func (i *Config) setExistingSystemDefaults() {
i.Lock()
defer i.Unlock()
if !i.isNewSystem {
// Existing systems as of the introduction of auto-browser open should retain existing
// behavior and not start the browser automatically.
if !i.main.Exists(NoBrowser) {
i.set(NoBrowser, true)
}
// Existing systems as of the introduction of the taskbar should inform users.
if !i.main.Exists(ShowOneTimeMovedNotification) {
i.set(ShowOneTimeMovedNotification, true)
}
}
}
// SetInitialConfig fills in missing required config fields. The config file will not be written.
func (i *Config) SetInitialConfig() error {
// generate some api keys
const apiKeyLength = 32
if string(i.GetJWTSignKey()) == "" {
signKey, err := hash.GenerateRandomKey(apiKeyLength)
if err != nil {
return fmt.Errorf("error generating JWTSignKey: %w", err)
}
i.SetString(JWTSignKey, signKey)
}
if string(i.GetSessionStoreKey()) == "" {
sessionStoreKey, err := hash.GenerateRandomKey(apiKeyLength)
if err != nil {
return fmt.Errorf("error generating session store key: %w", err)
}
i.SetString(SessionStoreKey, sessionStoreKey)
}
i.setDefaultValues()
return nil
}
func (i *Config) FinalizeSetup() {
i.isNewSystem = false
// i.configUpdates <- 0
}