Config: Refactor initialization of settings and database connection

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-07-07 17:00:38 +02:00
parent e2a7e50ca4
commit 22aa700b1e
28 changed files with 727 additions and 562 deletions

View File

@@ -214,7 +214,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
// Set user avatar image? // Set user avatar image?
if avatarUrl := userInfo.Picture; avatarUrl == "" || user.HasAvatar() { if avatarUrl := userInfo.Picture; avatarUrl == "" || user.HasAvatar() {
// Do nothing. // Do nothing.
} else if err = avatar.SetUserImageURL(user, avatarUrl, entity.SrcOIDC); err != nil { } else if err = avatar.SetUserImageURL(user, avatarUrl, entity.SrcOIDC, conf.ThumbCachePath()); err != nil {
event.AuditWarn([]string{clientIp, "create session", "oidc", userName, "failed to set avatar image", err.Error()}) event.AuditWarn([]string{clientIp, "create session", "oidc", userName, "failed to set avatar image", err.Error()})
} }
} else if conf.OIDCRegister() { } else if conf.OIDCRegister() {
@@ -287,7 +287,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
// Set user avatar image. // Set user avatar image.
if avatarUrl := userInfo.Picture; avatarUrl == "" { if avatarUrl := userInfo.Picture; avatarUrl == "" {
event.AuditDebug([]string{clientIp, "create session", "oidc", userName, "no avatar image provided"}) event.AuditDebug([]string{clientIp, "create session", "oidc", userName, "no avatar image provided"})
} else if err = avatar.SetUserImageURL(user, avatarUrl, entity.SrcOIDC); err != nil { } else if err = avatar.SetUserImageURL(user, avatarUrl, entity.SrcOIDC, conf.ThumbCachePath()); err != nil {
event.AuditWarn([]string{clientIp, "create session", "oidc", userName, "failed to set avatar image", err.Error()}) event.AuditWarn([]string{clientIp, "create session", "oidc", userName, "failed to set avatar image", err.Error()})
} }
} else { } else {

View File

@@ -122,7 +122,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
} }
// Set user avatar image. // Set user avatar image.
if err = avatar.SetUserImage(m, filePath, entity.SrcManual); err != nil { if err = avatar.SetUserImage(m, filePath, entity.SrcManual, conf.ThumbCachePath()); err != nil {
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err) event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
} }

View File

@@ -7,7 +7,7 @@ import (
// init initializes the package. // init initializes the package.
func init() { func init() {
// Register OpenID Connect extension. // Register OpenID Connect extension.
config.Register("oidc", UpdateConfig, ClientConfig) config.Register("oidc", InitConfig, ClientConfig)
} }
// ClientConfig returns the OIDC client config values. // ClientConfig returns the OIDC client config values.
@@ -24,7 +24,7 @@ func ClientConfig(c *config.Config, t config.ClientType) config.Map {
return result return result
} }
// UpdateConfig initializes the OIDC config options. // InitConfig initializes the OIDC config options.
func UpdateConfig(c *config.Config) error { func InitConfig(c *config.Config) error {
return nil return nil
} }

View File

@@ -83,7 +83,6 @@ func backupAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
if backupDatabase { if backupDatabase {

View File

@@ -120,7 +120,6 @@ func CallWithDependencies(ctx *cli.Context, action func(conf *config.Config) err
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
// Run command. // Run command.

View File

@@ -48,7 +48,6 @@ func convertAction(ctx *cli.Context) error {
return config.ErrReadOnly return config.ErrReadOnly
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
convertPath := conf.OriginalsPath() convertPath := conf.OriginalsPath()

View File

@@ -38,7 +38,6 @@ func findAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
frm := form.SearchPhotos{ frm := form.SearchPhotos{

View File

@@ -64,7 +64,6 @@ func migrationsStatusAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
var ids []string var ids []string
@@ -153,7 +152,6 @@ func migrationsRunAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
if ctx.Bool("trace") { if ctx.Bool("trace") {

View File

@@ -49,7 +49,6 @@ func resetAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
if !ctx.Bool("yes") { if !ctx.Bool("yes") {

View File

@@ -75,7 +75,6 @@ func restoreAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
// Restore database from backup dump? // Restore database from backup dump?

View File

@@ -42,7 +42,6 @@ func thumbsAction(ctx *cli.Context) error {
return err return err
} }
conf.RegisterDb()
defer conf.Shutdown() defer conf.Shutdown()
dir := strings.TrimSpace(ctx.Args().First()) dir := strings.TrimSpace(ctx.Args().First())

View File

@@ -28,7 +28,6 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -128,7 +127,7 @@ func NewConfig(ctx *cli.Context) *Config {
// Initialize logger. // Initialize logger.
initLogger() initLogger()
// Initialize options from config file and CLI context. // Initialize options from the "defaults.yml" file and CLI context.
c := &Config{ c := &Config{
cliCtx: ctx, cliCtx: ctx,
options: NewOptions(ctx), options: NewOptions(ctx),
@@ -137,7 +136,7 @@ func NewConfig(ctx *cli.Context) *Config {
start: start, start: start,
} }
// WriteFile values with options.yml from config path. // Override options with values from the "options.yml" file, if it exists.
if optionsYaml := c.OptionsYaml(); fs.FileExists(optionsYaml) { if optionsYaml := c.OptionsYaml(); fs.FileExists(optionsYaml) {
if err := c.options.Load(optionsYaml); err != nil { if err := c.options.Load(optionsYaml); err != nil {
log.Warnf("config: failed loading values from %s (%s)", clean.Log(optionsYaml), err) log.Warnf("config: failed loading values from %s (%s)", clean.Log(optionsYaml), err)
@@ -146,102 +145,9 @@ func NewConfig(ctx *cli.Context) *Config {
} }
} }
Ext().Init(c)
return c return c
} }
// Unsafe checks if unsafe settings are allowed.
func (c *Config) Unsafe() bool {
return c.options.Unsafe
}
// Restart checks if the application should be restarted, e.g. after an update or a config changes.
func (c *Config) Restart() bool {
return mutex.Restart.Load()
}
// CliContext returns the cli context if set.
func (c *Config) CliContext() *cli.Context {
if c.cliCtx == nil {
log.Warnf("config: cli context not set - you may have found a bug")
}
return c.cliCtx
}
// CliGlobalString returns a global cli string flag value if set.
func (c *Config) CliGlobalString(name string) string {
if c.cliCtx == nil {
return ""
}
return c.cliCtx.GlobalString(name)
}
// Options returns the raw config options.
func (c *Config) Options() *Options {
if c.options == nil {
log.Warnf("config: options should not be nil - you may have found a bug")
c.options = NewOptions(nil)
}
return c.options
}
// Propagate updates config options in other packages as needed.
func (c *Config) Propagate() {
FlushCache()
log.SetLevel(c.LogLevel())
// Initialize the thumbnail generation package.
thumb.Library = c.ThumbLibrary()
thumb.Color = c.ThumbColor()
thumb.Filter = c.ThumbFilter()
thumb.SizeCached = c.ThumbSizePrecached()
thumb.SizeOnDemand = c.ThumbSizeUncached()
thumb.JpegQualityDefault = c.JpegQuality()
thumb.CachePublic = c.HttpCachePublic()
// Set cache expiration defaults.
ttl.CacheDefault = c.HttpCacheMaxAge()
ttl.CacheVideo = c.HttpVideoMaxAge()
// Set geocoding parameters.
places.UserAgent = c.UserAgent()
entity.GeoApi = c.GeoApi()
// Set session cache duration.
entity.SessionCacheDuration = c.SessionCacheDuration()
// Set minimum password length.
entity.PasswordLength = c.PasswordLength()
// Set path for user assets.
entity.UsersPath = c.UsersPath()
// Set API preview and download default tokens.
entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig)
entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig)
entity.ValidateTokens = !c.Public()
// Set face recognition parameters.
face.ScoreThreshold = c.FaceScore()
face.OverlapThreshold = c.FaceOverlap()
face.ClusterScoreThreshold = c.FaceClusterScore()
face.ClusterSizeThreshold = c.FaceClusterSize()
face.ClusterCore = c.FaceClusterCore()
face.ClusterDist = c.FaceClusterDist()
face.MatchDist = c.FaceMatchDist()
// Set default theme and locale.
customize.DefaultTheme = c.DefaultTheme()
customize.DefaultLocale = c.DefaultLocale()
c.Settings().Propagate()
c.Hub().Propagate()
}
// Init creates directories, parses additional config files, opens a database connection and initializes dependencies. // Init creates directories, parses additional config files, opens a database connection and initializes dependencies.
func (c *Config) Init() error { func (c *Config) Init() error {
start := time.Now() start := time.Now()
@@ -304,11 +210,18 @@ func (c *Config) Init() error {
_ = os.Setenv("HTTPS_PROXY", httpsProxy) _ = os.Setenv("HTTPS_PROXY", httpsProxy)
} }
// Configure HTTP user agent. // Load settings from the "settings.yml" config file.
places.UserAgent = c.UserAgent()
c.initSettings() c.initSettings()
c.initHub()
// Connect to database.
if err := c.connectDb(); err != nil {
return err
} else {
c.RegisterDb()
}
// Initialize extensions.
Ext().Init(c)
// Initialize the thumbnail generation package. // Initialize the thumbnail generation package.
thumb.Init(memory.FreeMemory(), c.IndexWorkers(), c.ThumbLibrary()) thumb.Init(memory.FreeMemory(), c.IndexWorkers(), c.ThumbLibrary())
@@ -316,10 +229,8 @@ func (c *Config) Init() error {
// Update package defaults. // Update package defaults.
c.Propagate() c.Propagate()
// Connect to database. // Show support information.
if err := c.connectDb(); err != nil { if !c.Sponsor() {
return err
} else if !c.Sponsor() {
log.Info(MsgSponsor) log.Info(MsgSponsor)
log.Info(MsgSignUp) log.Info(MsgSignUp)
} }
@@ -330,6 +241,97 @@ func (c *Config) Init() error {
return nil return nil
} }
// Propagate updates config options in other packages as needed.
func (c *Config) Propagate() {
FlushCache()
log.SetLevel(c.LogLevel())
// Initialize the thumbnail generation package.
thumb.Library = c.ThumbLibrary()
thumb.Color = c.ThumbColor()
thumb.Filter = c.ThumbFilter()
thumb.SizeCached = c.ThumbSizePrecached()
thumb.SizeOnDemand = c.ThumbSizeUncached()
thumb.JpegQualityDefault = c.JpegQuality()
thumb.CachePublic = c.HttpCachePublic()
// Set cache expiration defaults.
ttl.CacheDefault = c.HttpCacheMaxAge()
ttl.CacheVideo = c.HttpVideoMaxAge()
// Set geocoding parameters.
places.UserAgent = c.UserAgent()
entity.GeoApi = c.GeoApi()
// Set session cache duration.
entity.SessionCacheDuration = c.SessionCacheDuration()
// Set minimum password length.
entity.PasswordLength = c.PasswordLength()
// Set path for user assets.
entity.UsersPath = c.UsersPath()
// Set API preview and download default tokens.
entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig)
entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig)
entity.ValidateTokens = !c.Public()
// Set face recognition parameters.
face.ScoreThreshold = c.FaceScore()
face.OverlapThreshold = c.FaceOverlap()
face.ClusterScoreThreshold = c.FaceClusterScore()
face.ClusterSizeThreshold = c.FaceClusterSize()
face.ClusterCore = c.FaceClusterCore()
face.ClusterDist = c.FaceClusterDist()
face.MatchDist = c.FaceMatchDist()
// Set default theme and locale.
customize.DefaultTheme = c.DefaultTheme()
customize.DefaultLocale = c.DefaultLocale()
c.Settings().Propagate()
c.Hub().Propagate()
}
// Options returns the raw config options.
func (c *Config) Options() *Options {
if c.options == nil {
log.Warnf("config: options should not be nil - you may have found a bug")
c.options = NewOptions(nil)
}
return c.options
}
// Unsafe checks if unsafe settings are allowed.
func (c *Config) Unsafe() bool {
return c.options.Unsafe
}
// Restart checks if the application should be restarted, e.g. after an update or a config changes.
func (c *Config) Restart() bool {
return mutex.Restart.Load()
}
// CliContext returns the cli context if set.
func (c *Config) CliContext() *cli.Context {
if c.cliCtx == nil {
log.Warnf("config: cli context not set - you may have found a bug")
}
return c.cliCtx
}
// CliGlobalString returns a global cli string flag value if set.
func (c *Config) CliGlobalString(name string) string {
if c.cliCtx == nil {
return ""
}
return c.cliCtx.GlobalString(name)
}
// readSerial reads and returns the current storage serial. // readSerial reads and returns the current storage serial.
func (c *Config) readSerial() string { func (c *Config) readSerial() string {
storageName := filepath.Join(c.StoragePath(), serialName) storageName := filepath.Join(c.StoragePath(), serialName)
@@ -437,192 +439,6 @@ func (c *Config) Copyright() string {
return c.options.Copyright return c.options.Copyright
} }
// 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
}
return strings.TrimRight(u.EscapedPath(), "/") + res
}
// 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 {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return res
}
return strings.TrimRight(c.options.CdnUrl, "/") + res
}
// UseCdn checks if a Content Deliver Network (CDN) is used to serve static content.
func (c *Config) UseCdn() bool {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return false
}
return true
}
// NoCdn checks if there is no Content Deliver Network (CDN) configured to serve static content.
func (c *Config) NoCdn() bool {
return !c.UseCdn()
}
// CdnDomain returns the content delivery network domain name if specified.
func (c *Config) CdnDomain() string {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return ""
} else if u, err := url.Parse(c.options.CdnUrl); err != nil {
return ""
} else {
return u.Hostname()
}
}
// CdnVideo checks if videos should be streamed using the configured CDN.
func (c *Config) CdnVideo() bool {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return false
}
return c.options.CdnVideo
}
// CORSOrigin returns the value for the Access-Control-Allow-Origin header, if any.
func (c *Config) CORSOrigin() string {
return clean.Header(c.options.CORSOrigin)
}
// CORSHeaders returns the value for the Access-Control-Allow-Headers header, if any.
func (c *Config) CORSHeaders() string {
return clean.Header(c.options.CORSHeaders)
}
// CORSMethods returns the value for the Access-Control-Allow-Methods header, if any.
func (c *Config) CORSMethods() string {
return clean.Header(c.options.CORSMethods)
}
// ContentUri returns the content delivery URI.
func (c *Config) ContentUri() string {
return c.CdnUrl(c.ApiUri())
}
// VideoUri returns the video streaming URI.
func (c *Config) VideoUri() string {
if c.CdnVideo() {
return c.ContentUri()
}
return c.ApiUri()
}
// StaticUri returns the static content URI.
func (c *Config) StaticUri() string {
return c.CdnUrl(c.BaseUri(StaticUri))
}
// StaticAssetUri returns the resource URI of the static file asset.
func (c *Config) StaticAssetUri(res string) string {
return c.StaticUri() + "/" + res
}
// SiteUrl returns the public server URL (default is "http://localhost:2342/").
func (c *Config) SiteUrl() string {
if c.options.SiteUrl == "" {
return "http://localhost:2342/"
}
return strings.TrimRight(c.options.SiteUrl, "/") + "/"
}
// 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://")
}
// 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()
}
}
// SiteAuthor returns the site author / copyright.
func (c *Config) SiteAuthor() string {
return c.options.SiteAuthor
}
// SiteTitle returns the main site title (default is application name).
func (c *Config) SiteTitle() string {
if c.options.SiteTitle == "" {
return c.Name()
}
return c.options.SiteTitle
}
// SiteCaption returns a short site caption.
func (c *Config) SiteCaption() string {
return c.options.SiteCaption
}
// SiteDescription returns a long site description.
func (c *Config) SiteDescription() string {
return c.options.SiteDescription
}
// SitePreview returns the site preview image URL for sharing.
func (c *Config) SitePreview() string {
if c.options.SitePreview == "" {
return fmt.Sprintf("https://i.photoprism.app/prism?cover=64&style=centered%%20dark&caption=none&title=%s", url.QueryEscape(c.AppName()))
}
if !strings.HasPrefix(c.options.SitePreview, "http") {
return c.SiteUrl() + strings.TrimPrefix(c.options.SitePreview, "/")
}
return c.options.SitePreview
}
// LegalInfo returns the legal info text for the page footer.
func (c *Config) LegalInfo() string {
if s := c.CliGlobalString("imprint"); s != "" {
log.Warnf("config: option 'imprint' is deprecated, please use 'legal-info'")
return s
}
return c.options.LegalInfo
}
// LegalUrl returns the legal info url.
func (c *Config) LegalUrl() string {
if s := c.CliGlobalString("imprint-url"); s != "" {
log.Warnf("config: option 'imprint-url' is deprecated, please use 'legal-url'")
return s
}
return c.options.LegalUrl
}
// Prod checks if production mode is enabled, hides non-essential log messages. // Prod checks if production mode is enabled, hides non-essential log messages.
func (c *Config) Prod() bool { func (c *Config) Prod() bool {
return c.options.Prod return c.options.Prod
@@ -679,16 +495,6 @@ func (c *Config) ReadOnly() bool {
return c.options.ReadOnly return c.options.ReadOnly
} }
// DetectNSFW checks if NSFW photos should be detected and flagged.
func (c *Config) DetectNSFW() bool {
return c.options.DetectNSFW
}
// UploadNSFW checks if NSFW photos can be uploaded.
func (c *Config) UploadNSFW() bool {
return c.options.UploadNSFW
}
// LogLevel returns the Logrus log level. // LogLevel returns the Logrus log level.
func (c *Config) LogLevel() logrus.Level { func (c *Config) LogLevel() logrus.Level {
// Normalize string. // Normalize string.

View File

@@ -16,12 +16,22 @@ func (c *Config) TensorFlowModelPath() string {
return filepath.Join(c.AssetsPath(), "nasnet") return filepath.Join(c.AssetsPath(), "nasnet")
} }
// FaceNetModelPath returns the FaceNet model path.
func (c *Config) FaceNetModelPath() string {
return filepath.Join(c.AssetsPath(), "facenet")
}
// NSFWModelPath returns the "not safe for work" TensorFlow model path. // NSFWModelPath returns the "not safe for work" TensorFlow model path.
func (c *Config) NSFWModelPath() string { func (c *Config) NSFWModelPath() string {
return filepath.Join(c.AssetsPath(), "nsfw") return filepath.Join(c.AssetsPath(), "nsfw")
} }
// FaceNetModelPath returns the FaceNet model path. // DetectNSFW checks if NSFW photos should be detected and flagged.
func (c *Config) FaceNetModelPath() string { func (c *Config) DetectNSFW() bool {
return filepath.Join(c.AssetsPath(), "facenet") return c.options.DetectNSFW
}
// UploadNSFW checks if NSFW photos can be uploaded.
func (c *Config) UploadNSFW() bool {
return c.options.UploadNSFW
} }

View File

@@ -0,0 +1,47 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfig_TensorFlowVersion(t *testing.T) {
c := NewConfig(CliTestContext())
version := c.TensorFlowVersion()
assert.IsType(t, "1.15.0", version)
}
func TestConfig_TensorFlowModelPath(t *testing.T) {
c := NewConfig(CliTestContext())
path := c.TensorFlowModelPath()
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/nasnet", path)
}
func TestConfig_TensorFlowDisabled(t *testing.T) {
c := NewConfig(CliTestContext())
version := c.DisableTensorFlow()
assert.Equal(t, false, version)
}
func TestConfig_NSFWModelPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Contains(t, c.NSFWModelPath(), "/assets/nsfw")
}
func TestConfig_FaceNetModelPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Contains(t, c.FaceNetModelPath(), "/assets/facenet")
}
func TestConfig_DetectNSFW(t *testing.T) {
c := NewConfig(CliTestContext())
result := c.DetectNSFW()
assert.Equal(t, true, result)
}

View File

@@ -0,0 +1,66 @@
package config
import (
"net/url"
"strings"
"github.com/photoprism/photoprism/pkg/clean"
)
// CdnUrl returns the optional content delivery network URI without trailing slash.
func (c *Config) CdnUrl(res string) string {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return res
}
return strings.TrimRight(c.options.CdnUrl, "/") + res
}
// UseCdn checks if a Content Deliver Network (CDN) is used to serve static content.
func (c *Config) UseCdn() bool {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return false
}
return true
}
// NoCdn checks if there is no Content Deliver Network (CDN) configured to serve static content.
func (c *Config) NoCdn() bool {
return !c.UseCdn()
}
// CdnDomain returns the content delivery network domain name if specified.
func (c *Config) CdnDomain() string {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return ""
} else if u, err := url.Parse(c.options.CdnUrl); err != nil {
return ""
} else {
return u.Hostname()
}
}
// CdnVideo checks if videos should be streamed using the configured CDN.
func (c *Config) CdnVideo() bool {
if c.options.CdnUrl == "" || c.options.CdnUrl == c.options.SiteUrl {
return false
}
return c.options.CdnVideo
}
// CORSOrigin returns the value for the Access-Control-Allow-Origin header, if any.
func (c *Config) CORSOrigin() string {
return clean.Header(c.options.CORSOrigin)
}
// CORSHeaders returns the value for the Access-Control-Allow-Headers header, if any.
func (c *Config) CORSHeaders() string {
return clean.Header(c.options.CORSHeaders)
}
// CORSMethods returns the value for the Access-Control-Allow-Methods header, if any.
func (c *Config) CORSMethods() string {
return clean.Header(c.options.CORSMethods)
}

View File

@@ -0,0 +1,104 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/header"
)
func TestConfig_CdnUrl(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.options.SiteUrl)
assert.Equal(t, "", c.CdnUrl(""))
assert.True(t, c.NoCdn())
assert.False(t, c.UseCdn())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "/", c.CdnUrl("/"))
c.options.CdnUrl = "http://foo:2342/foo/"
assert.Equal(t, "http://foo:2342/foo", c.CdnUrl(""))
assert.Equal(t, "http://foo:2342/foo/", c.CdnUrl("/"))
assert.False(t, c.NoCdn())
assert.True(t, c.UseCdn())
c.options.SiteUrl = c.options.CdnUrl
assert.Equal(t, "/", c.CdnUrl("/"))
assert.Equal(t, "", c.CdnUrl(""))
assert.True(t, c.NoCdn())
assert.False(t, c.UseCdn())
c.options.SiteUrl = ""
assert.False(t, c.NoCdn())
assert.True(t, c.UseCdn())
}
func TestConfig_CdnDomain(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.options.SiteUrl)
assert.Equal(t, "", c.CdnDomain())
c.options.CdnUrl = "http://superhost:2342/"
assert.Equal(t, "superhost", c.CdnDomain())
c.options.CdnUrl = "https://foo.bar.com:2342/foo/"
assert.Equal(t, "foo.bar.com", c.CdnDomain())
c.options.SiteUrl = c.options.CdnUrl
assert.Equal(t, "", c.CdnDomain())
c.options.SiteUrl = ""
c.options.CdnUrl = "http:/invalid:2342/foo/"
assert.Equal(t, "", c.CdnDomain())
c.options.CdnUrl = ""
assert.Equal(t, "", c.CdnDomain())
}
func TestConfig_CdnVideo(t *testing.T) {
c := NewConfig(CliTestContext())
assert.False(t, c.CdnVideo())
c.options.SiteUrl = "http://superhost:2342/"
assert.False(t, c.CdnVideo())
c.options.CdnUrl = "http://foo:2342/foo/"
assert.False(t, c.CdnVideo())
c.options.CdnVideo = true
assert.True(t, c.CdnVideo())
c.options.SiteUrl = c.options.CdnUrl
assert.False(t, c.CdnVideo())
c.options.SiteUrl = ""
assert.True(t, c.CdnVideo())
c.options.CdnVideo = false
assert.False(t, c.CdnVideo())
c.options.CdnUrl = ""
assert.False(t, c.CdnVideo())
}
func TestConfig_CORSOrigin(t *testing.T) {
c := NewConfig(CliTestContext())
c.Options().CORSOrigin = ""
assert.Equal(t, "", c.CORSOrigin())
c.Options().CORSOrigin = "*"
assert.Equal(t, "*", c.CORSOrigin())
c.Options().CORSOrigin = "https://developer.mozilla.org"
assert.Equal(t, "https://developer.mozilla.org", c.CORSOrigin())
c.Options().CORSOrigin = ""
assert.Equal(t, "", c.CORSOrigin())
}
func TestConfig_CORSHeaders(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CORSHeaders())
c.Options().CORSHeaders = header.DefaultAccessControlAllowHeaders
assert.Equal(t, header.DefaultAccessControlAllowHeaders, c.CORSHeaders())
c.Options().CORSHeaders = ""
assert.Equal(t, "", c.CORSHeaders())
}
func TestConfig_CORSMethods(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CORSMethods())
c.Options().CORSMethods = header.DefaultAccessControlAllowMethods
assert.Equal(t, header.DefaultAccessControlAllowMethods, c.CORSMethods())
c.Options().CORSMethods = ""
assert.Equal(t, "", c.CORSMethods())
}

View File

@@ -0,0 +1,135 @@
package config
import (
"fmt"
"net/url"
"strings"
)
// 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
}
return strings.TrimRight(u.EscapedPath(), "/") + res
}
// ApiUri returns the api URI.
func (c *Config) ApiUri() string {
return c.BaseUri(ApiUri)
}
// ContentUri returns the content delivery URI based on the CdnUrl and the ApiUri.
func (c *Config) ContentUri() string {
return c.CdnUrl(c.ApiUri())
}
// VideoUri returns the video streaming URI.
func (c *Config) VideoUri() string {
if c.CdnVideo() {
return c.ContentUri()
}
return c.ApiUri()
}
// StaticUri returns the static content URI.
func (c *Config) StaticUri() string {
return c.CdnUrl(c.BaseUri(StaticUri))
}
// StaticAssetUri returns the resource URI of the static file asset.
func (c *Config) StaticAssetUri(res string) string {
return c.StaticUri() + "/" + res
}
// SiteUrl returns the public server URL (default is "http://localhost:2342/").
func (c *Config) SiteUrl() string {
if c.options.SiteUrl == "" {
return "http://localhost:2342/"
}
return strings.TrimRight(c.options.SiteUrl, "/") + "/"
}
// 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://")
}
// 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()
}
}
// SiteAuthor returns the site author / copyright.
func (c *Config) SiteAuthor() string {
return c.options.SiteAuthor
}
// SiteTitle returns the main site title (default is application name).
func (c *Config) SiteTitle() string {
if c.options.SiteTitle == "" {
return c.Name()
}
return c.options.SiteTitle
}
// SiteCaption returns a short site caption.
func (c *Config) SiteCaption() string {
return c.options.SiteCaption
}
// SiteDescription returns a long site description.
func (c *Config) SiteDescription() string {
return c.options.SiteDescription
}
// SitePreview returns the site preview image URL for sharing.
func (c *Config) SitePreview() string {
if c.options.SitePreview == "" {
return fmt.Sprintf("https://i.photoprism.app/prism?cover=64&style=centered%%20dark&caption=none&title=%s", url.QueryEscape(c.AppName()))
}
if !strings.HasPrefix(c.options.SitePreview, "http") {
return c.SiteUrl() + strings.TrimPrefix(c.options.SitePreview, "/")
}
return c.options.SitePreview
}
// LegalInfo returns the legal info text for the page footer.
func (c *Config) LegalInfo() string {
if s := c.CliGlobalString("imprint"); s != "" {
log.Warnf("config: option 'imprint' is deprecated, please use 'legal-info'")
return s
}
return c.options.LegalInfo
}
// LegalUrl returns the legal info url.
func (c *Config) LegalUrl() string {
if s := c.CliGlobalString("imprint-url"); s != "" {
log.Warnf("config: option 'imprint-url' is deprecated, please use 'legal-url'")
return s
}
return c.options.LegalUrl
}

View File

@@ -0,0 +1,143 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfig_BaseUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.BaseUri(""))
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "", c.BaseUri(""))
c.options.SiteUrl = "http://foo:2342/foo bar/"
assert.Equal(t, "/foo%20bar", c.BaseUri(""))
assert.Equal(t, "/foo%20bar/baz", c.BaseUri("/baz"))
}
func TestConfig_StaticUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "/static", c.StaticUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "/static", c.StaticUri())
c.options.SiteUrl = "http://foo:2342/foo/"
assert.Equal(t, "/foo/static", c.StaticUri())
c.options.CdnUrl = "http://foo:2342/bar"
assert.Equal(t, "http://foo:2342/bar/foo"+StaticUri, c.StaticUri())
}
func TestConfig_ApiUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ApiUri, c.ApiUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, ApiUri, c.ApiUri())
c.options.SiteUrl = "http://foo:2342/foo/"
assert.Equal(t, "/foo"+ApiUri, c.ApiUri())
}
func TestConfig_ContentUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ApiUri, c.ContentUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, ApiUri, c.ContentUri())
c.options.CdnUrl = "http://foo:2342//"
assert.Equal(t, "http://foo:2342"+ApiUri, c.ContentUri())
}
func TestConfig_VideoUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ApiUri, c.VideoUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, ApiUri, c.VideoUri())
c.options.CdnUrl = "http://foo:2342//"
c.options.CdnVideo = true
assert.Equal(t, "http://foo:2342"+ApiUri, c.VideoUri())
c.options.CdnVideo = false
assert.Equal(t, ApiUri, c.VideoUri())
}
func TestConfig_SiteUrl(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "http://localhost:2342/", c.SiteUrl())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "http://superhost:2342/", c.SiteUrl())
c.options.SiteUrl = "http://superhost"
assert.Equal(t, "http://superhost/", c.SiteUrl())
}
func TestConfig_SiteHttps(t *testing.T) {
t.Run("Default", func(t *testing.T) {
c := NewConfig(CliTestContext())
assert.False(t, c.SiteHttps())
})
}
func TestConfig_SiteDomain(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "localhost", c.SiteDomain())
c.options.SiteUrl = "https://foo.bar.com:2342/"
assert.Equal(t, "foo.bar.com", c.SiteDomain())
c.options.SiteUrl = ""
assert.Equal(t, "localhost", c.SiteDomain())
}
func TestConfig_SitePreview(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "https://i.photoprism.app/prism?cover=64&style=centered%20dark&caption=none&title=PhotoPrism", c.SitePreview())
c.options.SitePreview = "http://preview.jpg"
assert.Equal(t, "http://preview.jpg", c.SitePreview())
c.options.SitePreview = "preview123.jpg"
assert.Equal(t, "http://localhost:2342/preview123.jpg", c.SitePreview())
c.options.SitePreview = "foo/preview123.jpg"
assert.Equal(t, "http://localhost:2342/foo/preview123.jpg", c.SitePreview())
c.options.SitePreview = "/foo/preview123.jpg"
assert.Equal(t, "http://localhost:2342/foo/preview123.jpg", c.SitePreview())
}
func TestConfig_SiteAuthor(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.SiteAuthor())
c.options.SiteAuthor = "@Jens.Mander"
assert.Equal(t, "@Jens.Mander", c.SiteAuthor())
c.options.SiteAuthor = ""
assert.Equal(t, "", c.SiteAuthor())
}
func TestConfig_SiteTitle(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "PhotoPrism", c.SiteTitle())
c.options.SiteTitle = "Cats"
assert.Equal(t, "Cats", c.SiteTitle())
c.options.SiteTitle = "PhotoPrism"
assert.Equal(t, "PhotoPrism", c.SiteTitle())
}
func TestConfig_SiteCaption(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.SiteCaption())
c.options.SiteCaption = "PhotoPrism App"
assert.Equal(t, "PhotoPrism App", c.SiteCaption())
c.options.SiteCaption = ""
assert.Equal(t, "", c.SiteCaption())
}
func TestConfig_SiteDescription(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.SiteDescription())
c.options.SiteDescription = "My Description!"
assert.Equal(t, "My Description!", c.SiteDescription())
c.options.SiteDescription = ""
assert.Equal(t, "", c.SiteDescription())
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -97,20 +96,6 @@ func TestConfig_VersionChecksum(t *testing.T) {
assert.Equal(t, uint32(0x2e5b4b86), c.VersionChecksum()) assert.Equal(t, uint32(0x2e5b4b86), c.VersionChecksum())
} }
func TestConfig_TensorFlowVersion(t *testing.T) {
c := NewConfig(CliTestContext())
version := c.TensorFlowVersion()
assert.IsType(t, "1.15.0", version)
}
func TestConfig_TensorFlowDisabled(t *testing.T) {
c := NewConfig(CliTestContext())
version := c.DisableTensorFlow()
assert.Equal(t, false, version)
}
func TestConfig_Copyright(t *testing.T) { func TestConfig_Copyright(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
@@ -207,13 +192,6 @@ func TestConfig_CustomAssetsPath(t *testing.T) {
assert.Equal(t, "", c.CustomAssetsPath()) assert.Equal(t, "", c.CustomAssetsPath())
} }
func TestConfig_DetectNSFW(t *testing.T) {
c := NewConfig(CliTestContext())
result := c.DetectNSFW()
assert.Equal(t, true, result)
}
func TestConfig_AdminUser(t *testing.T) { func TestConfig_AdminUser(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
@@ -230,18 +208,6 @@ func TestConfig_AdminPassword(t *testing.T) {
assert.Equal(t, "photoprism", result) assert.Equal(t, "photoprism", result)
} }
func TestConfig_NSFWModelPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Contains(t, c.NSFWModelPath(), "/assets/nsfw")
}
func TestConfig_FaceNetModelPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Contains(t, c.FaceNetModelPath(), "/assets/facenet")
}
func TestConfig_ExamplesPath(t *testing.T) { func TestConfig_ExamplesPath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
@@ -249,13 +215,6 @@ func TestConfig_ExamplesPath(t *testing.T) {
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/examples", path) assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/examples", path)
} }
func TestConfig_TensorFlowModelPath(t *testing.T) {
c := NewConfig(CliTestContext())
path := c.TensorFlowModelPath()
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/nasnet", path)
}
func TestConfig_TemplatesPath(t *testing.T) { func TestConfig_TemplatesPath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
@@ -383,198 +342,6 @@ func TestConfig_ResolutionLimit(t *testing.T) {
assert.Equal(t, -1, c.ResolutionLimit()) assert.Equal(t, -1, c.ResolutionLimit())
} }
func TestConfig_BaseUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.BaseUri(""))
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "", c.BaseUri(""))
c.options.SiteUrl = "http://foo:2342/foo bar/"
assert.Equal(t, "/foo%20bar", c.BaseUri(""))
assert.Equal(t, "/foo%20bar/baz", c.BaseUri("/baz"))
}
func TestConfig_StaticUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "/static", c.StaticUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "/static", c.StaticUri())
c.options.SiteUrl = "http://foo:2342/foo/"
assert.Equal(t, "/foo/static", c.StaticUri())
c.options.CdnUrl = "http://foo:2342/bar"
assert.Equal(t, "http://foo:2342/bar/foo"+StaticUri, c.StaticUri())
}
func TestConfig_ApiUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ApiUri, c.ApiUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, ApiUri, c.ApiUri())
c.options.SiteUrl = "http://foo:2342/foo/"
assert.Equal(t, "/foo"+ApiUri, c.ApiUri())
}
func TestConfig_CdnUrl(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.options.SiteUrl)
assert.Equal(t, "", c.CdnUrl(""))
assert.True(t, c.NoCdn())
assert.False(t, c.UseCdn())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "/", c.CdnUrl("/"))
c.options.CdnUrl = "http://foo:2342/foo/"
assert.Equal(t, "http://foo:2342/foo", c.CdnUrl(""))
assert.Equal(t, "http://foo:2342/foo/", c.CdnUrl("/"))
assert.False(t, c.NoCdn())
assert.True(t, c.UseCdn())
c.options.SiteUrl = c.options.CdnUrl
assert.Equal(t, "/", c.CdnUrl("/"))
assert.Equal(t, "", c.CdnUrl(""))
assert.True(t, c.NoCdn())
assert.False(t, c.UseCdn())
c.options.SiteUrl = ""
assert.False(t, c.NoCdn())
assert.True(t, c.UseCdn())
}
func TestConfig_CdnDomain(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.options.SiteUrl)
assert.Equal(t, "", c.CdnDomain())
c.options.CdnUrl = "http://superhost:2342/"
assert.Equal(t, "superhost", c.CdnDomain())
c.options.CdnUrl = "https://foo.bar.com:2342/foo/"
assert.Equal(t, "foo.bar.com", c.CdnDomain())
c.options.SiteUrl = c.options.CdnUrl
assert.Equal(t, "", c.CdnDomain())
c.options.SiteUrl = ""
c.options.CdnUrl = "http:/invalid:2342/foo/"
assert.Equal(t, "", c.CdnDomain())
c.options.CdnUrl = ""
assert.Equal(t, "", c.CdnDomain())
}
func TestConfig_CdnVideo(t *testing.T) {
c := NewConfig(CliTestContext())
assert.False(t, c.CdnVideo())
c.options.SiteUrl = "http://superhost:2342/"
assert.False(t, c.CdnVideo())
c.options.CdnUrl = "http://foo:2342/foo/"
assert.False(t, c.CdnVideo())
c.options.CdnVideo = true
assert.True(t, c.CdnVideo())
c.options.SiteUrl = c.options.CdnUrl
assert.False(t, c.CdnVideo())
c.options.SiteUrl = ""
assert.True(t, c.CdnVideo())
c.options.CdnVideo = false
assert.False(t, c.CdnVideo())
c.options.CdnUrl = ""
assert.False(t, c.CdnVideo())
}
func TestConfig_CORSOrigin(t *testing.T) {
c := NewConfig(CliTestContext())
c.Options().CORSOrigin = ""
assert.Equal(t, "", c.CORSOrigin())
c.Options().CORSOrigin = "*"
assert.Equal(t, "*", c.CORSOrigin())
c.Options().CORSOrigin = "https://developer.mozilla.org"
assert.Equal(t, "https://developer.mozilla.org", c.CORSOrigin())
c.Options().CORSOrigin = ""
assert.Equal(t, "", c.CORSOrigin())
}
func TestConfig_CORSHeaders(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CORSHeaders())
c.Options().CORSHeaders = header.DefaultAccessControlAllowHeaders
assert.Equal(t, header.DefaultAccessControlAllowHeaders, c.CORSHeaders())
c.Options().CORSHeaders = ""
assert.Equal(t, "", c.CORSHeaders())
}
func TestConfig_CORSMethods(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CORSMethods())
c.Options().CORSMethods = header.DefaultAccessControlAllowMethods
assert.Equal(t, header.DefaultAccessControlAllowMethods, c.CORSMethods())
c.Options().CORSMethods = ""
assert.Equal(t, "", c.CORSMethods())
}
func TestConfig_ContentUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ApiUri, c.ContentUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, ApiUri, c.ContentUri())
c.options.CdnUrl = "http://foo:2342//"
assert.Equal(t, "http://foo:2342"+ApiUri, c.ContentUri())
}
func TestConfig_VideoUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ApiUri, c.VideoUri())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, ApiUri, c.VideoUri())
c.options.CdnUrl = "http://foo:2342//"
c.options.CdnVideo = true
assert.Equal(t, "http://foo:2342"+ApiUri, c.VideoUri())
c.options.CdnVideo = false
assert.Equal(t, ApiUri, c.VideoUri())
}
func TestConfig_SiteUrl(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "http://localhost:2342/", c.SiteUrl())
c.options.SiteUrl = "http://superhost:2342/"
assert.Equal(t, "http://superhost:2342/", c.SiteUrl())
c.options.SiteUrl = "http://superhost"
assert.Equal(t, "http://superhost/", c.SiteUrl())
}
func TestConfig_SiteDomain(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "localhost", c.SiteDomain())
c.options.SiteUrl = "https://foo.bar.com:2342/"
assert.Equal(t, "foo.bar.com", c.SiteDomain())
c.options.SiteUrl = ""
assert.Equal(t, "localhost", c.SiteDomain())
}
func TestConfig_SitePreview(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "https://i.photoprism.app/prism?cover=64&style=centered%20dark&caption=none&title=PhotoPrism", c.SitePreview())
c.options.SitePreview = "http://preview.jpg"
assert.Equal(t, "http://preview.jpg", c.SitePreview())
c.options.SitePreview = "preview123.jpg"
assert.Equal(t, "http://localhost:2342/preview123.jpg", c.SitePreview())
c.options.SitePreview = "foo/preview123.jpg"
assert.Equal(t, "http://localhost:2342/foo/preview123.jpg", c.SitePreview())
c.options.SitePreview = "/foo/preview123.jpg"
assert.Equal(t, "http://localhost:2342/foo/preview123.jpg", c.SitePreview())
}
func TestConfig_SiteTitle(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "PhotoPrism", c.SiteTitle())
c.options.SiteTitle = "Cats"
assert.Equal(t, "Cats", c.SiteTitle())
}
func TestConfig_Serial(t *testing.T) { func TestConfig_Serial(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())

View File

@@ -234,7 +234,7 @@ func NewOptions(ctx *cli.Context) *Options {
c.BackupDatabase = true c.BackupDatabase = true
c.BackupAlbums = true c.BackupAlbums = true
// Load defaults from YAML file? // Initialize options with the values from the "defaults.yml" file, if it exists.
if defaultsYaml := ctx.GlobalString("defaults-yaml"); defaultsYaml == "" { if defaultsYaml := ctx.GlobalString("defaults-yaml"); defaultsYaml == "" {
log.Tracef("config: defaults file was not specified") log.Tracef("config: defaults file was not specified")
} else if c.DefaultsYaml = fs.Abs(defaultsYaml); !fs.FileExists(c.DefaultsYaml) { } else if c.DefaultsYaml = fs.Abs(defaultsYaml); !fs.FileExists(c.DefaultsYaml) {
@@ -243,6 +243,7 @@ func NewOptions(ctx *cli.Context) *Options {
log.Warnf("config: failed loading defaults from %s (%s)", clean.Log(c.DefaultsYaml), err) log.Warnf("config: failed loading defaults from %s (%s)", clean.Log(c.DefaultsYaml), err)
} }
// Apply options specified with environment variables and command-line flags.
if err := c.ApplyCliContext(ctx); err != nil { if err := c.ApplyCliContext(ctx); err != nil {
log.Error(err) log.Error(err)
} }

View File

@@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
) )
// initSettings initializes user settings from a config file. // initSettings initializes the customization settings from the "settings.yml" file.
func (c *Config) initSettings() { func (c *Config) initSettings() {
if c.settings != nil { if c.settings != nil {
return return
@@ -43,6 +43,7 @@ func (c *Config) initSettings() {
// Settings returns the global app settings. // Settings returns the global app settings.
func (c *Config) Settings() *customize.Settings { func (c *Config) Settings() *customize.Settings {
// Load settings from the "settings.yml" config file.
c.initSettings() c.initSettings()
if c.DisablePlaces() { if c.DisablePlaces() {

View File

@@ -202,6 +202,35 @@ var UserFixtures = UserMap{
BirthYear: 2001, BirthYear: 2001,
}, },
}, },
"guest": {
ID: 10000025,
UserUID: "usg73p55zwgr1gbq",
UserName: "guest",
DisplayName: "Guest User",
UserEmail: "guest@example.com",
UserRole: acl.RoleGuest.String(),
AuthProvider: authn.ProviderOIDC.String(),
AuthMethod: authn.MethodDefault.String(),
SuperAdmin: false,
CanLogin: true,
WebDAV: false,
CanInvite: false,
InviteToken: "",
UserSettings: &UserSettings{
UITheme: "default",
MapsStyle: "default",
MapsAnimate: 6250,
UILanguage: "en",
UITimeZone: "UTC",
},
UserDetails: &UserDetails{
NickName: "Gustav",
UserGender: GenderMale,
BirthDay: 23,
BirthMonth: 1,
BirthYear: 1999,
},
},
} }
// CreateUserFixtures creates the user fixtures specified above // CreateUserFixtures creates the user fixtures specified above

View File

@@ -11,12 +11,37 @@ import (
// RegisteredUsers finds all registered users. // RegisteredUsers finds all registered users.
func RegisteredUsers() (result entity.Users) { func RegisteredUsers() (result entity.Users) {
if err := Db().Where("id > 0").Find(&result).Error; err != nil { if err := Db().Where("id > 0").Find(&result).Error; err != nil {
log.Errorf("users: %s", err) log.Errorf("users: %s (find)", err)
} }
return result return result
} }
// CountUsers returns the number of users based on the specified filter options.
func CountUsers(registered, active bool, roles, excludeRoles []string) (count int) {
stmt := Db().Model(entity.Users{})
if registered {
stmt = stmt.Where("id > 0")
}
if active {
stmt = stmt.Where("(can_login > 0 OR webdav > 0) AND user_name <> ''")
}
if len(roles) > 0 {
stmt = stmt.Where("user_role IN (?)", roles)
} else if len(excludeRoles) > 0 {
stmt = stmt.Where("user_role NOT IN (?)", excludeRoles)
}
if err := stmt.Count(&count).Error; err != nil {
log.Errorf("users: %s (count)", err)
}
return count
}
// Users finds users and returns them. // Users finds users and returns them.
func Users(limit, offset int, sortOrder, search string) (result entity.Users, err error) { func Users(limit, offset int, sortOrder, search string) (result entity.Users, err error) {
result = entity.Users{} result = entity.Users{}

View File

@@ -19,6 +19,30 @@ func TestRegisteredUsers(t *testing.T) {
}) })
} }
func TestCountUsers(t *testing.T) {
t.Run("All", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(false, false, nil, nil), 10)
})
t.Run("Registered", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(true, false, nil, nil), 8)
})
t.Run("Active", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(false, true, nil, nil), 8)
})
t.Run("RegisteredActive", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(true, true, nil, nil), 8)
})
t.Run("Admins", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(true, true, []string{"admin"}, nil), 6)
})
t.Run("NoAdmins", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(true, true, []string{}, []string{"admin"}), 2)
})
t.Run("Guests", func(t *testing.T) {
assert.LessOrEqual(t, CountUsers(true, true, []string{"guest"}, nil), 2)
})
}
func TestUsers(t *testing.T) { func TestUsers(t *testing.T) {
t.Run("Default", func(t *testing.T) { t.Run("Default", func(t *testing.T) {
if results, err := Users(0, 0, "", ""); err != nil { if results, err := Users(0, 0, "", ""); err != nil {

View File

@@ -13,8 +13,10 @@ import (
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
// FeedbackURL is the service endpoint for submitting user feedback.
var FeedbackURL = ServiceURL + "/%s/feedback" var FeedbackURL = ServiceURL + "/%s/feedback"
// Feedback represents user feedback submitted through the user interface.
type Feedback struct { type Feedback struct {
Category string `json:"Category"` Category string `json:"Category"`
Subject string `json:"Subject"` Subject string `json:"Subject"`

View File

@@ -5,6 +5,7 @@ import (
"runtime" "runtime"
) )
// ServiceURL specifies the service endpoint URL.
var ServiceURL = "https://my.photoprism.app/v1/hello" var ServiceURL = "https://my.photoprism.app/v1/hello"
// Request represents basic environment specs for debugging. // Request represents basic environment specs for debugging.
@@ -15,10 +16,16 @@ type Request struct {
ClientArch string `json:"ClientArch"` ClientArch string `json:"ClientArch"`
ClientCPU int `json:"ClientCPU"` ClientCPU int `json:"ClientCPU"`
ClientEnv string `json:"ClientEnv"` ClientEnv string `json:"ClientEnv"`
ClientOpt string `json:"ClientOpt"`
PartnerID string `json:"PartnerID"` PartnerID string `json:"PartnerID"`
ApiToken string `json:"ApiToken"` ApiToken string `json:"ApiToken"`
} }
// ClientOpt returns a custom request option.
var ClientOpt = func() string {
return ""
}
// NewRequest creates a new backend key request instance. // NewRequest creates a new backend key request instance.
func NewRequest(version, serial, env, partnerId, token string) *Request { func NewRequest(version, serial, env, partnerId, token string) *Request {
return &Request{ return &Request{
@@ -28,6 +35,7 @@ func NewRequest(version, serial, env, partnerId, token string) *Request {
ClientArch: runtime.GOARCH, ClientArch: runtime.GOARCH,
ClientCPU: runtime.NumCPU(), ClientCPU: runtime.NumCPU(),
ClientEnv: env, ClientEnv: env,
ClientOpt: ClientOpt(),
PartnerID: partnerId, PartnerID: partnerId,
ApiToken: token, ApiToken: token,
} }

View File

@@ -17,7 +17,7 @@ import (
) )
// SetUserImageURL sets a new user avatar URL. // SetUserImageURL sets a new user avatar URL.
func SetUserImageURL(m *entity.User, imageUrl, imageSrc string) error { func SetUserImageURL(m *entity.User, imageUrl, imageSrc, thumbPath string) error {
if imageUrl == "" { if imageUrl == "" {
return nil return nil
} }
@@ -52,7 +52,7 @@ func SetUserImageURL(m *entity.User, imageUrl, imageSrc string) error {
return fmt.Errorf("failed to rename avatar image (%w)", err) return fmt.Errorf("failed to rename avatar image (%w)", err)
} }
if err = SetUserImage(m, imageName, imageSrc); err != nil { if err = SetUserImage(m, imageName, imageSrc, thumbPath); err != nil {
return fmt.Errorf("failed to set avatar image (%w)", err) return fmt.Errorf("failed to set avatar image (%w)", err)
} }
@@ -64,14 +64,16 @@ func SetUserImageURL(m *entity.User, imageUrl, imageSrc string) error {
} }
// SetUserImage sets a new user avatar image. // SetUserImage sets a new user avatar image.
func SetUserImage(m *entity.User, imageName, imageSrc string) error { func SetUserImage(m *entity.User, imageName, imageSrc, thumbPath string) error {
var conf *config.Config var conf *config.Config
if conf = get.Config(); conf == nil { if conf = get.Config(); conf == nil {
return fmt.Errorf("config required") return fmt.Errorf("config required")
} }
thumbPath := conf.ThumbCachePath() if thumbPath == "" {
thumbPath = conf.ThumbCachePath()
}
if mediaFile, mediaErr := photoprism.NewMediaFile(imageName); mediaErr != nil { if mediaFile, mediaErr := photoprism.NewMediaFile(imageName); mediaErr != nil {
return mediaErr return mediaErr

View File

@@ -10,31 +10,35 @@ import (
) )
func TestSetUserAvatarURL(t *testing.T) { func TestSetUserAvatarURL(t *testing.T) {
thumbPath := fs.Abs("testdata/cache")
t.Run("PNG", func(t *testing.T) { t.Run("PNG", func(t *testing.T) {
admin := entity.UserFixtures.Get("alice") admin := entity.UserFixtures.Get("alice")
imageUrl := "https://dl.photoprism.app/icons/logo/256.png" imageUrl := "https://dl.photoprism.app/icons/logo/256.png"
err := SetUserImageURL(&admin, imageUrl, entity.SrcAuto) err := SetUserImageURL(&admin, imageUrl, entity.SrcAuto, thumbPath)
assert.NoError(t, err) assert.NoError(t, err)
}) })
t.Run("JPEG", func(t *testing.T) { t.Run("JPEG", func(t *testing.T) {
admin := entity.UserFixtures.Get("bob") admin := entity.UserFixtures.Get("bob")
imageUrl := "https://dl.photoprism.app/img/team/avatar.jpg" imageUrl := "https://dl.photoprism.app/img/team/avatar.jpg"
err := SetUserImageURL(&admin, imageUrl, entity.SrcOIDC) err := SetUserImageURL(&admin, imageUrl, entity.SrcOIDC, thumbPath)
assert.NoError(t, err) assert.NoError(t, err)
}) })
t.Run("NotFound", func(t *testing.T) { t.Run("NotFound", func(t *testing.T) {
admin := entity.UserFixtures.Get("alice") admin := entity.UserFixtures.Get("alice")
imageUrl := "https://dl.photoprism.app/img/team/avatar-invalid.jpg" imageUrl := "https://dl.photoprism.app/img/team/avatar-invalid.jpg"
err := SetUserImageURL(&admin, imageUrl, entity.SrcAuto) err := SetUserImageURL(&admin, imageUrl, entity.SrcAuto, thumbPath)
assert.Error(t, err) assert.Error(t, err)
}) })
} }
func TestSetUserAvatarImage(t *testing.T) { func TestSetUserAvatarImage(t *testing.T) {
thumbPath := fs.Abs("testdata/cache")
t.Run("Admin", func(t *testing.T) { t.Run("Admin", func(t *testing.T) {
admin := entity.UserFixtures.Get("friend") admin := entity.UserFixtures.Get("friend")
fileName := fs.Abs("testdata/avatar.png") fileName := fs.Abs("testdata/avatar.png")
err := SetUserImage(&admin, fileName, entity.SrcAuto) err := SetUserImage(&admin, fileName, entity.SrcAuto, thumbPath)
assert.NoError(t, err) assert.NoError(t, err)
}) })
} }