2019-05-06 23:18:10 +02:00
|
|
|
package config
|
2019-05-03 18:57:28 +02:00
|
|
|
|
|
|
|
import (
|
2021-01-02 15:08:39 +01:00
|
|
|
"encoding/hex"
|
2020-09-06 14:18:40 +02:00
|
|
|
"fmt"
|
2021-01-02 15:08:39 +01:00
|
|
|
"hash/crc32"
|
2021-07-05 16:41:43 +02:00
|
|
|
"net/url"
|
2020-12-05 06:21:16 +01:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2020-01-02 00:03:07 +01:00
|
|
|
"runtime"
|
2020-06-26 14:26:36 +02:00
|
|
|
"strings"
|
2020-01-31 15:29:06 +01:00
|
|
|
"sync"
|
2019-05-03 18:57:28 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/jinzhu/gorm"
|
|
|
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
|
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
2021-09-23 13:16:05 +02:00
|
|
|
|
|
|
|
"github.com/dustin/go-humanize"
|
2021-01-09 12:18:59 +01:00
|
|
|
"github.com/klauspost/cpuid/v2"
|
2021-09-23 13:16:05 +02:00
|
|
|
"github.com/pbnjay/memory"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/urfave/cli"
|
|
|
|
|
2022-09-28 09:01:17 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/customize"
|
2021-09-23 13:16:05 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/entity"
|
2019-12-02 00:30:58 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
2021-09-23 13:16:05 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/face"
|
2020-12-04 13:10:32 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/hub"
|
|
|
|
"github.com/photoprism/photoprism/internal/hub/places"
|
2022-07-19 16:58:43 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/i18n"
|
2020-01-19 12:50:44 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/mutex"
|
2020-01-06 14:32:15 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/thumb"
|
2022-04-15 09:42:07 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
2021-09-23 13:16:05 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
2020-05-27 19:38:40 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
2019-05-03 18:57:28 +02:00
|
|
|
)
|
|
|
|
|
2019-12-02 00:30:58 +01:00
|
|
|
var log = event.Log
|
2020-04-13 18:08:21 +02:00
|
|
|
var once sync.Once
|
2021-08-05 15:15:33 +02:00
|
|
|
var LowMem = false
|
|
|
|
var TotalMem uint64
|
2019-12-02 00:30:58 +01:00
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Config holds database, cache and all parameters of photoprism
|
2019-05-06 23:18:10 +02:00
|
|
|
type Config struct {
|
2020-10-03 13:50:30 +02:00
|
|
|
once sync.Once
|
2022-10-11 22:44:11 +02:00
|
|
|
cliCtx *cli.Context
|
2020-12-18 20:42:12 +01:00
|
|
|
options *Options
|
2022-09-28 09:01:17 +02:00
|
|
|
settings *customize.Settings
|
2022-10-11 22:44:11 +02:00
|
|
|
db *gorm.DB
|
2020-12-04 13:10:32 +01:00
|
|
|
hub *hub.Config
|
2020-10-03 13:50:30 +02:00
|
|
|
token string
|
2020-12-05 06:21:16 +01:00
|
|
|
serial string
|
2022-03-02 14:16:49 +01:00
|
|
|
env string
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
|
2020-01-06 14:32:15 +01:00
|
|
|
func init() {
|
2021-08-05 15:15:33 +02:00
|
|
|
TotalMem = memory.TotalMemory()
|
2021-10-09 14:09:05 +02:00
|
|
|
|
2021-10-17 14:25:29 +02:00
|
|
|
// Check available memory if not running in unsafe mode.
|
2022-04-22 18:24:59 +02:00
|
|
|
if Env(EnvUnsafe) {
|
2021-10-17 14:25:29 +02:00
|
|
|
// Disable features with high memory requirements?
|
2021-10-09 14:09:05 +02:00
|
|
|
LowMem = TotalMem < MinMem
|
|
|
|
}
|
2021-08-05 15:15:33 +02:00
|
|
|
|
2020-07-13 15:23:54 +02:00
|
|
|
// Init public thumb sizes for use in client apps.
|
2022-07-06 23:01:54 +02:00
|
|
|
for i := len(thumb.Names) - 1; i >= 0; i-- {
|
|
|
|
name := thumb.Names[i]
|
2021-09-05 12:32:08 +02:00
|
|
|
t := thumb.Sizes[name]
|
2020-07-13 15:23:54 +02:00
|
|
|
|
2020-01-06 14:32:15 +01:00
|
|
|
if t.Public {
|
2021-09-05 13:48:53 +02:00
|
|
|
Thumbs = append(Thumbs, ThumbSize{Size: string(name), Use: t.Use, Width: t.Width, Height: t.Height})
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-22 17:38:40 +02:00
|
|
|
func initLogger() {
|
2020-01-31 15:29:06 +01:00
|
|
|
once.Do(func() {
|
|
|
|
log.SetFormatter(&logrus.TextFormatter{
|
|
|
|
DisableColors: false,
|
|
|
|
FullTimestamp: true,
|
|
|
|
})
|
|
|
|
|
2022-04-22 17:38:40 +02:00
|
|
|
if Env(EnvTrace) {
|
|
|
|
log.SetLevel(logrus.TraceLevel)
|
|
|
|
} else if Env(EnvDebug) {
|
2020-01-31 15:29:06 +01:00
|
|
|
log.SetLevel(logrus.DebugLevel)
|
|
|
|
} else {
|
|
|
|
log.SetLevel(logrus.InfoLevel)
|
|
|
|
}
|
2019-05-03 18:57:28 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// NewConfig initialises a new configuration file
|
2019-05-06 23:18:10 +02:00
|
|
|
func NewConfig(ctx *cli.Context) *Config {
|
2022-04-13 22:43:49 +02:00
|
|
|
// Initialize logger.
|
2022-04-22 17:38:40 +02:00
|
|
|
initLogger()
|
2019-05-03 18:57:28 +02:00
|
|
|
|
2022-04-13 22:43:49 +02:00
|
|
|
// Initialize options from config file and CLI context.
|
2019-05-14 16:04:17 +02:00
|
|
|
c := &Config{
|
2022-10-11 22:44:11 +02:00
|
|
|
cliCtx: ctx,
|
2020-12-18 20:42:12 +01:00
|
|
|
options: NewOptions(ctx),
|
2022-04-15 09:42:07 +02:00
|
|
|
token: rnd.GenerateToken(8),
|
2022-03-02 14:16:49 +01:00
|
|
|
env: os.Getenv("DOCKER_ENV"),
|
2020-12-18 20:42:12 +01:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:43:49 +02:00
|
|
|
// Overwrite values with options.yml from config path.
|
|
|
|
if optionsYaml := c.OptionsYaml(); fs.FileExists(optionsYaml) {
|
|
|
|
if err := c.options.Load(optionsYaml); err != nil {
|
2022-04-15 09:42:07 +02:00
|
|
|
log.Warnf("config: failed loading values from %s (%s)", clean.Log(optionsYaml), err)
|
2020-12-18 20:42:12 +01:00
|
|
|
} else {
|
2022-04-15 09:42:07 +02:00
|
|
|
log.Debugf("config: overriding config with values from %s", clean.Log(optionsYaml))
|
2020-12-18 20:42:12 +01:00
|
|
|
}
|
2019-05-14 16:04:17 +02:00
|
|
|
}
|
2019-05-03 18:57:28 +02:00
|
|
|
|
2022-10-09 17:16:49 +02:00
|
|
|
Ext().Init(c)
|
2022-07-05 23:13:34 +02:00
|
|
|
|
2020-04-13 18:08:21 +02:00
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2022-04-03 14:51:58 +02:00
|
|
|
// Unsafe checks if unsafe settings are allowed.
|
|
|
|
func (c *Config) Unsafe() bool {
|
|
|
|
return c.options.Unsafe
|
|
|
|
}
|
|
|
|
|
2022-10-11 22:44:11 +02:00
|
|
|
// CliContext returns the cli context if set.
|
|
|
|
func (c *Config) CliContext() *cli.Context {
|
|
|
|
if c.cliCtx == nil {
|
|
|
|
log.Warnf("config: cli context not set - possible bug")
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.cliCtx
|
|
|
|
}
|
|
|
|
|
2020-12-18 20:42:12 +01:00
|
|
|
// Options returns the raw config options.
|
|
|
|
func (c *Config) Options() *Options {
|
|
|
|
if c.options == nil {
|
2022-04-06 17:46:41 +02:00
|
|
|
log.Warnf("config: options should not be nil - possible bug")
|
2020-12-18 20:42:12 +01:00
|
|
|
c.options = NewOptions(nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.options
|
|
|
|
}
|
|
|
|
|
|
|
|
// Propagate updates config options in other packages as needed.
|
2020-04-13 18:08:21 +02:00
|
|
|
func (c *Config) Propagate() {
|
2019-05-03 18:57:28 +02:00
|
|
|
log.SetLevel(c.LogLevel())
|
|
|
|
|
2021-09-23 13:16:05 +02:00
|
|
|
// Set thumbnail generation parameters.
|
2022-04-06 18:41:15 +02:00
|
|
|
thumb.StandardRGB = c.ThumbSRGB()
|
2021-09-05 12:32:08 +02:00
|
|
|
thumb.SizePrecached = c.ThumbSizePrecached()
|
2020-07-18 17:33:02 +02:00
|
|
|
thumb.SizeUncached = c.ThumbSizeUncached()
|
2020-05-05 17:17:19 +02:00
|
|
|
thumb.Filter = c.ThumbFilter()
|
2020-05-05 15:42:54 +02:00
|
|
|
thumb.JpegQuality = c.JpegQuality()
|
2021-09-23 13:16:05 +02:00
|
|
|
|
|
|
|
// Set geocoding parameters.
|
2020-09-06 14:18:40 +02:00
|
|
|
places.UserAgent = c.UserAgent()
|
2020-12-05 00:13:44 +01:00
|
|
|
entity.GeoApi = c.GeoApi()
|
2020-01-06 14:32:15 +01:00
|
|
|
|
2022-10-13 22:11:02 +02:00
|
|
|
// Set API preview and download default tokens.
|
|
|
|
entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig)
|
|
|
|
entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig)
|
|
|
|
entity.CheckTokens = !c.Public()
|
|
|
|
|
2022-07-19 16:58:43 +02:00
|
|
|
// Set face recognition parameters.
|
2021-09-23 13:16:05 +02:00
|
|
|
face.ScoreThreshold = c.FaceScore()
|
|
|
|
face.OverlapThreshold = c.FaceOverlap()
|
2021-10-05 10:12:48 +02:00
|
|
|
face.ClusterScoreThreshold = c.FaceClusterScore()
|
|
|
|
face.ClusterSizeThreshold = c.FaceClusterSize()
|
2021-09-23 13:16:05 +02:00
|
|
|
face.ClusterCore = c.FaceClusterCore()
|
|
|
|
face.ClusterDist = c.FaceClusterDist()
|
|
|
|
face.MatchDist = c.FaceMatchDist()
|
|
|
|
|
2020-04-13 18:08:21 +02:00
|
|
|
c.Settings().Propagate()
|
2020-12-04 13:10:32 +01:00
|
|
|
c.Hub().Propagate()
|
2020-04-13 18:08:21 +02:00
|
|
|
}
|
|
|
|
|
2020-10-08 08:52:03 +02:00
|
|
|
// Init creates directories, parses additional config files, opens a database connection and initializes dependencies.
|
|
|
|
func (c *Config) Init() error {
|
2022-01-05 18:15:39 +01:00
|
|
|
start := time.Now()
|
|
|
|
|
2020-10-08 08:52:03 +02:00
|
|
|
if err := c.CreateDirectories(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
if err := c.initSerial(); err != nil {
|
2020-12-05 06:21:16 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-12-26 18:06:54 +01:00
|
|
|
if insensitive, err := c.CaseInsensitive(); err != nil {
|
|
|
|
return err
|
|
|
|
} else if insensitive {
|
2020-12-26 18:30:04 +01:00
|
|
|
log.Infof("config: case-insensitive file system detected")
|
2020-12-26 18:06:54 +01:00
|
|
|
fs.IgnoreCase()
|
|
|
|
}
|
|
|
|
|
2021-01-09 12:18:59 +01:00
|
|
|
if cpuName := cpuid.CPU.BrandName; cpuName != "" {
|
2022-04-15 09:42:07 +02:00
|
|
|
log.Debugf("config: running on %s, %s memory detected", clean.Log(cpuid.CPU.BrandName), humanize.Bytes(TotalMem))
|
2021-08-05 15:15:33 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// Exit if less than 128 MB RAM was detected.
|
2021-08-05 15:15:33 +02:00
|
|
|
if TotalMem < 128*Megabyte {
|
|
|
|
return fmt.Errorf("config: %s of memory detected, %d GB required", humanize.Bytes(TotalMem), MinMem/Gigabyte)
|
2022-04-06 17:46:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Show warning if less than 1 GB RAM was detected.
|
|
|
|
if LowMem {
|
2021-08-05 19:10:53 +02:00
|
|
|
log.Warnf(`config: less than %d GB of memory detected, please upgrade if server becomes unstable or unresponsive`, MinMem/Gigabyte)
|
2022-04-06 17:46:41 +02:00
|
|
|
log.Warnf("config: tensorflow as well as indexing and conversion of RAW files have been disabled automatically")
|
2021-08-05 15:15:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Show swap info.
|
|
|
|
if TotalMem < RecommendedMem {
|
2021-08-05 21:57:01 +02:00
|
|
|
log.Infof("config: make sure your server has enough swap configured to prevent restarts when there are memory usage spikes")
|
2021-01-09 12:18:59 +01:00
|
|
|
}
|
|
|
|
|
2022-04-03 14:51:58 +02:00
|
|
|
// Show wakeup interval warning if face recognition is enabled
|
|
|
|
// and the worker runs less than once per hour.
|
|
|
|
if !c.DisableFaces() && !c.Unsafe() && c.WakeupInterval() > time.Hour {
|
|
|
|
log.Warnf("config: the wakeup interval is %s, but must be 1h or less for face recognition to work", c.WakeupInterval().String())
|
|
|
|
}
|
|
|
|
|
2022-03-02 14:16:49 +01:00
|
|
|
// Set HTTP user agent.
|
|
|
|
places.UserAgent = c.UserAgent()
|
2021-11-20 16:36:34 +01:00
|
|
|
|
2020-10-08 08:52:03 +02:00
|
|
|
c.initSettings()
|
2020-12-04 13:10:32 +01:00
|
|
|
c.initHub()
|
2020-10-08 08:52:03 +02:00
|
|
|
|
2020-04-13 18:08:21 +02:00
|
|
|
c.Propagate()
|
2020-10-08 08:52:03 +02:00
|
|
|
|
2022-07-14 22:35:42 +02:00
|
|
|
if err := c.connectDb(); err != nil {
|
|
|
|
return err
|
|
|
|
} else if !c.Sponsor() {
|
|
|
|
log.Info(MsgSponsor)
|
|
|
|
log.Info(MsgSignUp)
|
2022-01-05 18:15:39 +01:00
|
|
|
}
|
|
|
|
|
2022-07-14 22:35:42 +02:00
|
|
|
log.Debugf("config: successfully initialized [%s]", time.Since(start))
|
|
|
|
|
|
|
|
return nil
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// readSerial reads and returns the current storage serial.
|
|
|
|
func (c *Config) readSerial() string {
|
|
|
|
storageName := filepath.Join(c.StoragePath(), serialName)
|
|
|
|
backupName := filepath.Join(c.BackupPath(), serialName)
|
|
|
|
|
|
|
|
if fs.FileExists(storageName) {
|
|
|
|
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 {
|
|
|
|
return string(data)
|
|
|
|
} else {
|
2022-04-15 09:42:07 +02:00
|
|
|
log.Tracef("config: could not read %s (%s)", clean.Log(storageName), err)
|
2022-04-06 17:46:41 +02:00
|
|
|
}
|
2021-01-02 15:08:39 +01:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
if fs.FileExists(backupName) {
|
|
|
|
if data, err := os.ReadFile(backupName); err == nil && len(data) == 16 {
|
|
|
|
return string(data)
|
|
|
|
} else {
|
2022-04-15 09:42:07 +02:00
|
|
|
log.Tracef("config: could not read %s (%s)", clean.Log(backupName), err)
|
2022-04-06 17:46:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// initSerial initializes storage directories with a random serial.
|
|
|
|
func (c *Config) initSerial() (err error) {
|
|
|
|
if c.Serial() != "" {
|
|
|
|
return nil
|
|
|
|
}
|
2020-12-05 06:21:16 +01:00
|
|
|
|
2022-04-15 09:42:07 +02:00
|
|
|
c.serial = rnd.GenerateUID('z')
|
2020-12-05 06:21:16 +01:00
|
|
|
|
|
|
|
storageName := filepath.Join(c.StoragePath(), serialName)
|
|
|
|
backupName := filepath.Join(c.BackupPath(), serialName)
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
if err = os.WriteFile(storageName, []byte(c.serial), os.ModePerm); err != nil {
|
|
|
|
return fmt.Errorf("could not create %s: %s", storageName, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = os.WriteFile(backupName, []byte(c.serial), os.ModePerm); err != nil {
|
|
|
|
return fmt.Errorf("could not create %s: %s", backupName, err)
|
2020-12-05 06:21:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-01-02 15:08:39 +01:00
|
|
|
// Serial returns the random storage serial.
|
|
|
|
func (c *Config) Serial() string {
|
2022-04-06 17:46:41 +02:00
|
|
|
if c.serial == "" {
|
|
|
|
c.serial = c.readSerial()
|
2021-01-02 15:08:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.serial
|
|
|
|
}
|
|
|
|
|
|
|
|
// SerialChecksum returns the CRC32 checksum of the storage serial.
|
|
|
|
func (c *Config) SerialChecksum() string {
|
|
|
|
var result []byte
|
|
|
|
|
|
|
|
hash := crc32.New(crc32.MakeTable(crc32.Castagnoli))
|
|
|
|
|
|
|
|
if _, err := hash.Write([]byte(c.Serial())); err != nil {
|
|
|
|
log.Warnf("config: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return hex.EncodeToString(hash.Sum(result))
|
|
|
|
}
|
|
|
|
|
2022-05-20 19:27:33 +02:00
|
|
|
// Name returns the app name.
|
2019-05-06 23:18:10 +02:00
|
|
|
func (c *Config) Name() string {
|
2022-05-20 19:27:33 +02:00
|
|
|
if c.options.Name == "" {
|
|
|
|
return "PhotoPrism"
|
|
|
|
}
|
|
|
|
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Name
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
|
2022-05-20 19:27:33 +02:00
|
|
|
// Edition returns the app edition.
|
|
|
|
func (c *Config) Edition() string {
|
|
|
|
if c.options.Edition == "" {
|
|
|
|
return "PhotoPrism® Dev"
|
2022-05-20 21:31:29 +02:00
|
|
|
} else if strings.HasSuffix(c.options.Edition, "CE") && c.Sponsor() {
|
|
|
|
return strings.Replace(c.options.Edition, "CE", "Plus", 1)
|
2022-05-20 19:27:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.options.Edition
|
|
|
|
}
|
|
|
|
|
2020-05-31 02:09:52 +02:00
|
|
|
// Version returns the application version.
|
|
|
|
func (c *Config) Version() string {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Version
|
2020-05-31 02:09:52 +02:00
|
|
|
}
|
2019-12-11 14:10:20 +01:00
|
|
|
|
2022-03-02 14:16:49 +01:00
|
|
|
// UserAgent returns an HTTP user agent string based on the app config and version.
|
2020-09-06 14:18:40 +02:00
|
|
|
func (c *Config) UserAgent() string {
|
2022-03-16 17:34:09 +01:00
|
|
|
return fmt.Sprintf("%s/%s (%s)", c.Name(), c.Version(), strings.Join(append(c.Flags(), c.Serial()), "; "))
|
2020-09-06 14:18:40 +02:00
|
|
|
}
|
|
|
|
|
2020-05-31 02:09:52 +02:00
|
|
|
// Copyright returns the application copyright.
|
|
|
|
func (c *Config) Copyright() string {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Copyright
|
2019-12-11 14:10:20 +01:00
|
|
|
}
|
|
|
|
|
2021-07-05 16:41:43 +02:00
|
|
|
// BaseUri returns the site base URI for a given resource.
|
|
|
|
func (c *Config) BaseUri(res string) string {
|
|
|
|
if c.SiteUrl() == "" {
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
u, err := url.Parse(c.SiteUrl())
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2022-01-17 18:52:35 +01:00
|
|
|
return strings.TrimRight(u.EscapedPath(), "/") + res
|
2021-07-05 16:41:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ApiUri returns the api URI.
|
|
|
|
func (c *Config) ApiUri() string {
|
|
|
|
return c.BaseUri(ApiUri)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CdnUrl returns the optional content delivery network URI without trailing slash.
|
|
|
|
func (c *Config) CdnUrl(res string) string {
|
2022-04-22 17:38:40 +02:00
|
|
|
if c.NoSponsor() {
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2021-07-05 16:41:43 +02:00
|
|
|
return strings.TrimRight(c.options.CdnUrl, "/") + res
|
|
|
|
}
|
|
|
|
|
|
|
|
// ContentUri returns the content delivery URI.
|
|
|
|
func (c *Config) ContentUri() string {
|
|
|
|
return c.CdnUrl(c.ApiUri())
|
|
|
|
}
|
|
|
|
|
|
|
|
// StaticUri returns the static content URI.
|
|
|
|
func (c *Config) StaticUri() string {
|
|
|
|
return c.CdnUrl(c.BaseUri(StaticUri))
|
|
|
|
}
|
|
|
|
|
2022-10-20 16:19:28 +02:00
|
|
|
// SiteUrl returns the public server URL (default is "http://photoprism.me:2342/").
|
2020-05-31 02:09:52 +02:00
|
|
|
func (c *Config) SiteUrl() string {
|
2020-12-18 20:42:12 +01:00
|
|
|
if c.options.SiteUrl == "" {
|
2022-10-20 16:19:28 +02:00
|
|
|
return "http://photoprism.me:2342/"
|
2019-12-11 14:10:20 +01:00
|
|
|
}
|
|
|
|
|
2021-07-05 16:41:43 +02:00
|
|
|
return strings.TrimRight(c.options.SiteUrl, "/") + "/"
|
2019-12-11 14:10:20 +01:00
|
|
|
}
|
|
|
|
|
2022-10-11 22:44:11 +02:00
|
|
|
// SiteHttps checks if the site URL uses HTTPS.
|
|
|
|
func (c *Config) SiteHttps() bool {
|
|
|
|
if c.options.SiteUrl == "" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.HasPrefix(c.options.SiteUrl, "https://")
|
|
|
|
}
|
|
|
|
|
2021-12-14 15:47:30 +01:00
|
|
|
// SiteDomain returns the public server domain.
|
|
|
|
func (c *Config) SiteDomain() string {
|
|
|
|
if u, err := url.Parse(c.SiteUrl()); err != nil {
|
|
|
|
return "localhost"
|
|
|
|
} else {
|
|
|
|
return u.Hostname()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-05 22:33:29 +02:00
|
|
|
// SiteAuthor returns the site author / copyright.
|
|
|
|
func (c *Config) SiteAuthor() string {
|
|
|
|
return c.options.SiteAuthor
|
|
|
|
}
|
|
|
|
|
2020-05-31 02:09:52 +02:00
|
|
|
// SiteTitle returns the main site title (default is application name).
|
|
|
|
func (c *Config) SiteTitle() string {
|
2022-04-22 17:38:40 +02:00
|
|
|
if c.options.SiteTitle == "" || c.NoSponsor() {
|
2020-05-31 02:09:52 +02:00
|
|
|
return c.Name()
|
|
|
|
}
|
2019-12-11 14:10:20 +01:00
|
|
|
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.SiteTitle
|
2019-12-11 14:10:20 +01:00
|
|
|
}
|
|
|
|
|
2020-05-31 02:09:52 +02:00
|
|
|
// SiteCaption returns a short site caption.
|
|
|
|
func (c *Config) SiteCaption() string {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.SiteCaption
|
2019-12-11 14:10:20 +01:00
|
|
|
}
|
|
|
|
|
2020-05-31 02:09:52 +02:00
|
|
|
// SiteDescription returns a long site description.
|
|
|
|
func (c *Config) SiteDescription() string {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.SiteDescription
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
|
2021-11-21 16:36:42 +01:00
|
|
|
// SitePreview returns the site preview image URL for sharing.
|
|
|
|
func (c *Config) SitePreview() string {
|
2022-07-05 23:13:34 +02:00
|
|
|
if c.options.SitePreview == "" || c.NoSponsor() {
|
2021-11-21 16:36:42 +01:00
|
|
|
return c.SiteUrl() + "static/img/preview.jpg"
|
|
|
|
}
|
|
|
|
|
|
|
|
if !strings.HasPrefix(c.options.SitePreview, "http") {
|
|
|
|
return c.SiteUrl() + c.options.SitePreview
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.options.SitePreview
|
|
|
|
}
|
|
|
|
|
2022-02-08 14:41:03 +01:00
|
|
|
// Imprint returns the legal info text for the page footer.
|
|
|
|
func (c *Config) Imprint() string {
|
2022-04-22 17:38:40 +02:00
|
|
|
if c.NoSponsor() {
|
2022-02-08 14:41:03 +01:00
|
|
|
return MsgSponsor
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.options.Imprint
|
|
|
|
}
|
|
|
|
|
|
|
|
// ImprintUrl returns the legal info url.
|
|
|
|
func (c *Config) ImprintUrl() string {
|
2022-04-22 17:38:40 +02:00
|
|
|
if c.NoSponsor() {
|
2022-02-08 14:41:03 +01:00
|
|
|
return SignUpURL
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.options.ImprintUrl
|
|
|
|
}
|
|
|
|
|
2022-08-31 18:53:04 +02:00
|
|
|
// Prod checks if production mode is enabled, hides non-essential log messages.
|
|
|
|
func (c *Config) Prod() bool {
|
|
|
|
return c.options.Prod
|
|
|
|
}
|
|
|
|
|
2022-04-12 19:14:21 +02:00
|
|
|
// Debug checks if debug mode is enabled, shows non-essential log messages.
|
2019-05-06 23:18:10 +02:00
|
|
|
func (c *Config) Debug() bool {
|
2022-08-31 18:53:04 +02:00
|
|
|
if c.Prod() {
|
|
|
|
return false
|
|
|
|
} else if c.Trace() {
|
2022-04-12 19:14:21 +02:00
|
|
|
return true
|
|
|
|
}
|
2022-04-22 17:38:40 +02:00
|
|
|
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Debug
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
|
2022-04-12 19:14:21 +02:00
|
|
|
// Trace checks if trace mode is enabled, shows all log messages.
|
|
|
|
func (c *Config) Trace() bool {
|
2022-08-31 18:53:04 +02:00
|
|
|
if c.Prod() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-07-06 23:01:54 +02:00
|
|
|
return c.options.Trace || c.options.LogLevel == logrus.TraceLevel.String()
|
2022-04-12 19:14:21 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// Test checks if test mode is enabled.
|
2021-02-06 17:04:00 +01:00
|
|
|
func (c *Config) Test() bool {
|
|
|
|
return c.options.Test
|
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// Demo checks if demo mode is enabled.
|
2020-12-18 10:59:21 +01:00
|
|
|
func (c *Config) Demo() bool {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Demo
|
2020-12-18 10:59:21 +01:00
|
|
|
}
|
|
|
|
|
2022-07-14 22:35:42 +02:00
|
|
|
// Sponsor reports if you have chosen to support our mission.
|
2021-01-15 18:30:26 +01:00
|
|
|
func (c *Config) Sponsor() bool {
|
2022-07-14 23:34:54 +02:00
|
|
|
if Sponsor || c.options.Sponsor {
|
|
|
|
return true
|
|
|
|
} else if c.hub != nil {
|
|
|
|
Sponsor = c.Hub().Plus()
|
|
|
|
}
|
|
|
|
|
|
|
|
return Sponsor
|
2021-01-15 18:30:26 +01:00
|
|
|
}
|
|
|
|
|
2022-07-14 22:35:42 +02:00
|
|
|
// NoSponsor reports if you prefer not to support our mission.
|
2022-04-22 17:38:40 +02:00
|
|
|
func (c *Config) NoSponsor() bool {
|
|
|
|
return !c.Sponsor() && !c.Demo()
|
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// Experimental checks if experimental features should be enabled.
|
2019-12-30 12:38:11 +01:00
|
|
|
func (c *Config) Experimental() bool {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Experimental
|
2019-12-30 12:38:11 +01:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// ReadOnly checks if photo directories are write protected.
|
2019-05-06 23:18:10 +02:00
|
|
|
func (c *Config) ReadOnly() bool {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.ReadOnly
|
2019-05-04 09:11:33 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// DetectNSFW checks if NSFW photos should be detected and flagged.
|
2020-01-13 16:48:32 +01:00
|
|
|
func (c *Config) DetectNSFW() bool {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.DetectNSFW
|
2019-12-15 17:19:16 +01:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:46:41 +02:00
|
|
|
// UploadNSFW checks if NSFW photos can be uploaded.
|
2019-12-15 17:19:16 +01:00
|
|
|
func (c *Config) UploadNSFW() bool {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.UploadNSFW
|
2019-12-15 17:19:16 +01:00
|
|
|
}
|
|
|
|
|
2021-09-21 16:29:03 +02:00
|
|
|
// LogLevel returns the Logrus log level.
|
2019-12-02 00:30:58 +01:00
|
|
|
func (c *Config) LogLevel() logrus.Level {
|
2021-09-21 16:29:03 +02:00
|
|
|
// Normalize string.
|
|
|
|
c.options.LogLevel = strings.ToLower(strings.TrimSpace(c.options.LogLevel))
|
|
|
|
|
2022-04-12 19:14:21 +02:00
|
|
|
if c.Trace() {
|
|
|
|
c.options.LogLevel = logrus.TraceLevel.String()
|
|
|
|
} else if c.Debug() && c.options.LogLevel != logrus.TraceLevel.String() {
|
2021-09-21 16:29:03 +02:00
|
|
|
c.options.LogLevel = logrus.DebugLevel.String()
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
|
2020-12-18 20:42:12 +01:00
|
|
|
if logLevel, err := logrus.ParseLevel(c.options.LogLevel); err == nil {
|
2019-05-03 18:57:28 +02:00
|
|
|
return logLevel
|
|
|
|
} else {
|
2019-12-02 00:30:58 +01:00
|
|
|
return logrus.InfoLevel
|
2019-05-03 18:57:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-12 19:14:21 +02:00
|
|
|
// SetLogLevel sets the Logrus log level.
|
|
|
|
func (c *Config) SetLogLevel(level logrus.Level) {
|
|
|
|
log.SetLevel(level)
|
|
|
|
}
|
|
|
|
|
2020-01-19 12:50:44 +01:00
|
|
|
// Shutdown services and workers.
|
2019-05-06 23:18:10 +02:00
|
|
|
func (c *Config) Shutdown() {
|
2022-09-28 09:01:17 +02:00
|
|
|
mutex.CancelAll()
|
2020-01-19 12:50:44 +01:00
|
|
|
|
2019-05-04 17:34:51 +02:00
|
|
|
if err := c.CloseDb(); err != nil {
|
|
|
|
log.Errorf("could not close database connection: %s", err)
|
|
|
|
} else {
|
|
|
|
log.Info("closed database connection")
|
|
|
|
}
|
|
|
|
}
|
2019-11-12 04:34:37 +01:00
|
|
|
|
2020-01-02 00:03:07 +01:00
|
|
|
// Workers returns the number of workers e.g. for indexing files.
|
2020-01-02 04:08:33 +01:00
|
|
|
func (c *Config) Workers() int {
|
2021-08-05 15:15:33 +02:00
|
|
|
// Use one worker on systems with less than the recommended amount of memory.
|
|
|
|
if TotalMem < RecommendedMem {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
2021-01-11 13:00:46 +01:00
|
|
|
// NumCPU returns the number of logical CPU cores.
|
|
|
|
cores := runtime.NumCPU()
|
|
|
|
|
|
|
|
// Limit to physical cores to avoid high load on HT capable CPUs.
|
|
|
|
if cores > cpuid.CPU.PhysicalCores {
|
|
|
|
cores = cpuid.CPU.PhysicalCores
|
|
|
|
}
|
2020-03-09 00:51:10 +01:00
|
|
|
|
2021-12-09 07:47:23 +01:00
|
|
|
// Limit number of workers when using SQLite3 to avoid database locking issues.
|
|
|
|
if c.DatabaseDriver() == SQLite3 && (cores >= 8 && c.options.Workers <= 0 || c.options.Workers > 4) {
|
2020-12-07 16:20:35 +01:00
|
|
|
return 4
|
2020-10-21 07:33:24 +02:00
|
|
|
}
|
|
|
|
|
2021-01-11 13:00:46 +01:00
|
|
|
// Return explicit value if set and not too large.
|
|
|
|
if c.options.Workers > runtime.NumCPU() {
|
|
|
|
return runtime.NumCPU()
|
|
|
|
} else if c.options.Workers > 0 {
|
2020-12-18 20:42:12 +01:00
|
|
|
return c.options.Workers
|
2020-01-06 23:43:19 +01:00
|
|
|
}
|
|
|
|
|
2021-01-11 13:00:46 +01:00
|
|
|
// Use half the available cores by default.
|
2021-01-09 12:18:59 +01:00
|
|
|
if cores > 1 {
|
|
|
|
return cores / 2
|
2020-03-09 00:51:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return 1
|
2019-11-12 04:34:37 +01:00
|
|
|
}
|
2020-01-06 06:59:35 +01:00
|
|
|
|
2022-04-03 14:51:58 +02:00
|
|
|
// WakeupInterval returns the duration between background worker runs
|
|
|
|
// required for face recognition and index maintenance(1-86400s).
|
2020-04-06 22:09:45 +02:00
|
|
|
func (c *Config) WakeupInterval() time.Duration {
|
2022-04-03 14:51:58 +02:00
|
|
|
if c.options.WakeupInterval <= 0 {
|
|
|
|
if c.options.Unsafe {
|
|
|
|
// Worker can be disabled only in unsafe mode.
|
|
|
|
return time.Duration(0)
|
|
|
|
} else {
|
|
|
|
// Default to 15 minutes if no interval is set.
|
|
|
|
return DefaultWakeupInterval
|
|
|
|
}
|
2022-04-22 18:24:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Do not run more than once per minute.
|
|
|
|
if c.options.WakeupInterval < MinWakeupInterval/time.Second {
|
2022-04-22 18:06:45 +02:00
|
|
|
return MinWakeupInterval
|
2022-04-22 18:24:59 +02:00
|
|
|
} else if c.options.WakeupInterval < MinWakeupInterval {
|
|
|
|
c.options.WakeupInterval = c.options.WakeupInterval * time.Second
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do not run less than once per day.
|
|
|
|
if c.options.WakeupInterval > MaxWakeupInterval {
|
|
|
|
return MaxWakeupInterval
|
2020-04-06 22:09:45 +02:00
|
|
|
}
|
|
|
|
|
2022-04-03 14:51:58 +02:00
|
|
|
return c.options.WakeupInterval
|
2020-04-06 22:09:45 +02:00
|
|
|
}
|
|
|
|
|
2021-10-17 14:25:29 +02:00
|
|
|
// AutoIndex returns the auto index delay duration.
|
2021-01-02 18:56:15 +01:00
|
|
|
func (c *Config) AutoIndex() time.Duration {
|
|
|
|
if c.options.AutoIndex < 0 {
|
|
|
|
return time.Duration(0)
|
2021-10-17 14:25:29 +02:00
|
|
|
} else if c.options.AutoIndex == 0 || c.options.AutoIndex > 604800 {
|
2021-10-05 18:42:39 +02:00
|
|
|
return time.Duration(DefaultAutoIndexDelay) * time.Second
|
2021-01-02 18:56:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return time.Duration(c.options.AutoIndex) * time.Second
|
|
|
|
}
|
|
|
|
|
2021-10-17 14:25:29 +02:00
|
|
|
// AutoImport returns the auto import delay duration.
|
2021-01-02 18:56:15 +01:00
|
|
|
func (c *Config) AutoImport() time.Duration {
|
|
|
|
if c.options.AutoImport < 0 || c.ReadOnly() {
|
|
|
|
return time.Duration(0)
|
2021-10-17 14:25:29 +02:00
|
|
|
} else if c.options.AutoImport == 0 || c.options.AutoImport > 604800 {
|
2021-10-05 18:42:39 +02:00
|
|
|
return time.Duration(DefaultAutoImportDelay) * time.Second
|
2021-01-02 18:56:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return time.Duration(c.options.AutoImport) * time.Second
|
|
|
|
}
|
|
|
|
|
2021-10-05 18:42:39 +02:00
|
|
|
// GeoApi returns the preferred geocoding api (none or places).
|
2020-12-05 00:13:44 +01:00
|
|
|
func (c *Config) GeoApi() string {
|
2020-12-18 20:42:12 +01:00
|
|
|
if c.options.DisablePlaces {
|
2020-12-18 09:11:42 +01:00
|
|
|
return ""
|
2020-01-06 06:59:35 +01:00
|
|
|
}
|
2020-12-18 09:11:42 +01:00
|
|
|
|
|
|
|
return "places"
|
2020-01-06 06:59:35 +01:00
|
|
|
}
|
2020-05-25 19:10:44 +02:00
|
|
|
|
2022-04-02 18:04:02 +02:00
|
|
|
// OriginalsLimit returns the maximum size of originals in MB.
|
|
|
|
func (c *Config) OriginalsLimit() int {
|
2020-12-18 20:42:12 +01:00
|
|
|
if c.options.OriginalsLimit <= 0 || c.options.OriginalsLimit > 100000 {
|
2020-05-25 19:10:44 +02:00
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
2022-04-01 21:14:22 +02:00
|
|
|
return c.options.OriginalsLimit
|
|
|
|
}
|
|
|
|
|
|
|
|
// OriginalsLimitBytes returns the maximum size of originals in bytes.
|
|
|
|
func (c *Config) OriginalsLimitBytes() int64 {
|
2022-04-02 18:04:02 +02:00
|
|
|
if result := c.OriginalsLimit(); result <= 0 {
|
2022-04-01 21:14:22 +02:00
|
|
|
return -1
|
|
|
|
} else {
|
2022-04-02 18:04:02 +02:00
|
|
|
return int64(result) * 1024 * 1024
|
2022-04-01 21:14:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-02 18:04:02 +02:00
|
|
|
// ResolutionLimit returns the maximum resolution of originals in megapixels (width x height).
|
|
|
|
func (c *Config) ResolutionLimit() int {
|
2022-07-05 23:13:34 +02:00
|
|
|
if c.NoSponsor() {
|
2022-09-18 14:06:13 +02:00
|
|
|
return DefaultResolutionLimit
|
2022-07-05 23:13:34 +02:00
|
|
|
}
|
|
|
|
|
2022-04-02 18:04:02 +02:00
|
|
|
result := c.options.ResolutionLimit
|
2022-04-01 21:14:22 +02:00
|
|
|
|
2022-04-02 18:04:02 +02:00
|
|
|
if result <= 0 {
|
2022-04-01 21:14:22 +02:00
|
|
|
return -1
|
2022-04-02 18:04:02 +02:00
|
|
|
} else if result > 900 {
|
|
|
|
result = 900
|
2022-04-01 21:14:22 +02:00
|
|
|
}
|
|
|
|
|
2022-04-02 18:04:02 +02:00
|
|
|
return result
|
2020-05-25 19:10:44 +02:00
|
|
|
}
|
2020-10-03 13:50:30 +02:00
|
|
|
|
2022-07-19 16:58:43 +02:00
|
|
|
// UpdateHub renews backend api credentials for maps & places without a token.
|
2020-12-04 13:10:32 +01:00
|
|
|
func (c *Config) UpdateHub() {
|
2022-07-19 16:58:43 +02:00
|
|
|
_ = c.ResyncHub("")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResyncHub renews backend api credentials for maps & places with an optional token.
|
|
|
|
func (c *Config) ResyncHub(token string) error {
|
|
|
|
if err := c.hub.Resync(token); err != nil {
|
|
|
|
log.Debugf("config: %s (refresh backend api tokens)", err)
|
|
|
|
if token != "" {
|
|
|
|
return i18n.Error(i18n.ErrAccountConnect)
|
|
|
|
}
|
|
|
|
} else if err = c.hub.Save(); err != nil {
|
|
|
|
log.Debugf("config: %s (save backend api tokens)", err)
|
2020-10-04 22:22:53 +02:00
|
|
|
} else {
|
2020-12-04 13:10:32 +01:00
|
|
|
c.hub.Propagate()
|
2020-10-04 22:22:53 +02:00
|
|
|
}
|
2022-07-19 16:58:43 +02:00
|
|
|
|
|
|
|
return nil
|
2020-10-04 22:22:53 +02:00
|
|
|
}
|
|
|
|
|
2020-12-04 13:10:32 +01:00
|
|
|
// initHub initializes PhotoPrism hub config.
|
|
|
|
func (c *Config) initHub() {
|
2022-03-28 16:13:41 +02:00
|
|
|
if c.hub != nil {
|
|
|
|
return
|
2022-07-14 23:34:54 +02:00
|
|
|
} else if h := hub.NewConfig(c.Version(), c.HubConfigFile(), c.serial, c.env, c.UserAgent(), c.options.PartnerID); h != nil {
|
|
|
|
c.hub = h
|
2022-03-28 16:13:41 +02:00
|
|
|
}
|
|
|
|
|
2020-12-04 13:10:32 +01:00
|
|
|
if err := c.hub.Load(); err == nil {
|
2020-10-03 13:50:30 +02:00
|
|
|
// Do nothing.
|
2022-07-19 16:58:43 +02:00
|
|
|
} else if err := c.hub.Update(); err != nil {
|
|
|
|
log.Debugf("config: %s (init backend api tokens)", err)
|
2020-12-04 13:10:32 +01:00
|
|
|
} else if err := c.hub.Save(); err != nil {
|
2022-07-19 16:58:43 +02:00
|
|
|
log.Debugf("config: %s (save backend api tokens)", err)
|
2020-10-03 13:50:30 +02:00
|
|
|
}
|
|
|
|
|
2020-12-04 13:10:32 +01:00
|
|
|
c.hub.Propagate()
|
2020-10-04 04:47:54 +02:00
|
|
|
|
|
|
|
ticker := time.NewTicker(time.Hour * 24)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
2020-12-04 13:10:32 +01:00
|
|
|
c.UpdateHub()
|
2020-10-04 04:47:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2020-10-03 13:50:30 +02:00
|
|
|
}
|
|
|
|
|
2020-12-04 13:10:32 +01:00
|
|
|
// Hub returns the PhotoPrism hub config.
|
|
|
|
func (c *Config) Hub() *hub.Config {
|
2022-03-28 16:13:41 +02:00
|
|
|
c.initHub()
|
2020-10-08 08:52:03 +02:00
|
|
|
|
2020-12-04 13:10:32 +01:00
|
|
|
return c.hub
|
2020-10-03 13:50:30 +02:00
|
|
|
}
|