CI: Apply Go linter recommendations to "internal/config" packages #5330

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-11-22 20:00:53 +01:00
parent 57c9096d1f
commit 7c0f0b41ba
37 changed files with 219 additions and 133 deletions

View File

@@ -35,7 +35,7 @@ func ApplyCliContext(c interface{}, ctx *cli.Context) error {
if s == "" {
// Omit.
} else if sec := txt.UInt(s); sec > 0 {
fieldValue.Set(reflect.ValueOf(time.Duration(sec) * time.Second))
fieldValue.Set(reflect.ValueOf(time.Duration(sec) * time.Second)) //nolint:gosec // txt.UInt is bounded; duration uses int64 on supported platforms
} else if d, err := time.ParseDuration(s); err == nil {
fieldValue.Set(reflect.ValueOf(d))
}

View File

@@ -0,0 +1,45 @@
package config
import (
"flag"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
type durationTarget struct {
Interval time.Duration `flag:"interval"`
}
func TestApplyCliContext_Duration(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
}{
{name: "WithUnits", input: "1h30m", expected: 90 * time.Minute},
{name: "NumericSeconds", input: "30", expected: 30 * time.Second},
{name: "Invalid", input: "not-a-duration", expected: 0},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
flags := flag.NewFlagSet("test", flag.ContinueOnError)
flags.String("interval", "", "doc")
app := cli.NewApp()
ctx := cli.NewContext(app, flags, nil)
_ = ctx.Set("interval", tc.input)
target := &durationTarget{}
err := ApplyCliContext(target, ctx)
assert.NoError(t, err)
assert.Equal(t, tc.expected, target.Interval)
})
}
}

View File

@@ -99,15 +99,16 @@ func (f CliFlag) CommandFlag() string {
// Usage returns the command flag usage.
func (f CliFlag) Usage() string {
if list.Contains(f.Tags, EnvSponsor) {
switch {
case list.Contains(f.Tags, EnvSponsor):
return f.Flag.GetUsage() + "*members only*"
} else if list.Contains(f.Tags, Pro) {
case list.Contains(f.Tags, Pro):
return f.Flag.GetUsage() + "*pro*"
} else if list.Contains(f.Tags, Plus) {
case list.Contains(f.Tags, Plus):
return f.Flag.GetUsage() + "*plus*"
} else if list.Contains(f.Tags, Essentials) {
case list.Contains(f.Tags, Essentials):
return f.Flag.GetUsage() + "*essentials*"
} else {
default:
return f.Flag.GetUsage()
}
}

View File

@@ -38,7 +38,7 @@ func NewClientAssets(buildPath, baseUri string) *ClientAssets {
// Load loads the frontend assets from a webpack manifest file.
func (a *ClientAssets) Load(fileName string) error {
jsonFile, err := os.ReadFile(filepath.Join(a.BuildPath, fileName))
jsonFile, err := os.ReadFile(filepath.Join(a.BuildPath, fileName)) //nolint:gosec // path derived from configured assets directory
if err != nil {
return err
@@ -97,7 +97,7 @@ func (a *ClientAssets) SplashCssFile() string {
// SplashCssFileContents returns the splash screen CSS file contents for embedding in HTML.
func (a *ClientAssets) SplashCssFileContents() template.CSS {
return template.CSS(a.readFile(a.SplashCssFile()))
return template.CSS(a.readFile(a.SplashCssFile())) //nolint:gosec // assets are loaded from trusted local build output
}
// SplashJsUri returns the splash screen JS URI.
@@ -121,14 +121,14 @@ func (a *ClientAssets) SplashJsFileContents() template.JS {
if a.SplashJs == "" {
return ""
}
return template.JS(a.readFile(a.SplashJs))
return template.JS(a.readFile(a.SplashJs)) //nolint:gosec // assets are loaded from trusted local build output
}
// readFile reads the file contents and returns them as string.
func (a *ClientAssets) readFile(fileName string) string {
if fileName == "" {
return ""
} else if css, err := os.ReadFile(filepath.Join(a.BuildPath, fileName)); err != nil {
} else if css, err := os.ReadFile(filepath.Join(a.BuildPath, fileName)); err != nil { //nolint:gosec // path derived from configured assets directory
return ""
} else {
return string(bytes.TrimSpace(css))

View File

@@ -622,7 +622,7 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
// Exclude pictures in review from total count.
if c.Settings().Features.Review {
cfg.Count.All = cfg.Count.All - cfg.Count.Review
cfg.Count.All -= cfg.Count.Review
}
c.Db().
@@ -742,15 +742,16 @@ func (c *Config) ClientRole(role acl.Role) *ClientConfig {
// ClientSession provides the client config values for the specified session.
func (c *Config) ClientSession(sess *entity.Session) (cfg *ClientConfig) {
if sess.NoUser() && sess.IsClient() {
switch {
case sess.NoUser() && sess.IsClient():
cfg = c.ClientUser(false).ApplyACL(acl.Rules, sess.GetClientRole())
cfg.Settings = c.SessionSettings(sess)
} else if sess.GetUser().IsVisitor() {
case sess.GetUser().IsVisitor():
cfg = c.ClientShare()
} else if sess.GetUser().IsRegistered() {
case sess.GetUser().IsRegistered():
cfg = c.ClientUser(false).ApplyACL(acl.Rules, sess.GetUserRole())
cfg.Settings = c.SessionSettings(sess)
} else {
default:
cfg = c.ClientPublic()
}

View File

@@ -40,8 +40,8 @@ import (
"github.com/dustin/go-humanize"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
_ "github.com/jinzhu/gorm/dialects/mysql" // register mysql dialect
_ "github.com/jinzhu/gorm/dialects/sqlite" // register sqlite dialect
"github.com/klauspost/cpuid/v2"
gc "github.com/patrickmn/go-cache"
"github.com/pbnjay/memory"
@@ -69,7 +69,6 @@ import (
// Config aggregates CLI flags, options.yml overrides, runtime settings, and shared resources (database, caches) for the running instance.
type Config struct {
once sync.Once
cliCtx *cli.Context
options *Options
settings *customize.Settings
@@ -135,13 +134,14 @@ func initLogger() {
FullTimestamp: true,
})
if Env(EnvProd) {
switch {
case Env(EnvProd):
SetLogLevel(logrus.WarnLevel)
} else if Env(EnvTrace) {
case Env(EnvTrace):
SetLogLevel(logrus.TraceLevel)
} else if Env(EnvDebug) {
case Env(EnvDebug):
SetLogLevel(logrus.DebugLevel)
} else {
default:
SetLogLevel(logrus.InfoLevel)
}
})
@@ -239,7 +239,7 @@ func (c *Config) Init() error {
// Configure HTTPS proxy for outgoing connections.
if httpsProxy := c.HttpsProxy(); httpsProxy != "" {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.HttpsProxyInsecure(),
InsecureSkipVerify: c.HttpsProxyInsecure(), //nolint:gosec // proxy settings are user-configurable and opt-in
}
_ = os.Setenv("HTTPS_PROXY", httpsProxy)
@@ -454,7 +454,7 @@ func (c *Config) readSerial() string {
backupName := c.BackupPath(serialName)
if fs.FileExists(storageName) {
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 {
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 { //nolint:gosec // path is computed from config storage
return string(data)
} else {
log.Tracef("config: could not read %s (%s)", clean.Log(storageName), err)
@@ -462,7 +462,7 @@ func (c *Config) readSerial() string {
}
if fs.FileExists(backupName) {
if data, err := os.ReadFile(backupName); err == nil && len(data) == 16 {
if data, err := os.ReadFile(backupName); err == nil && len(data) == 16 { //nolint:gosec // backup file path is generated internally
return string(data)
} else {
log.Tracef("config: could not read %s (%s)", clean.Log(backupName), err)
@@ -729,7 +729,7 @@ func (c *Config) WakeupInterval() time.Duration {
if c.options.WakeupInterval < MinWakeupInterval/time.Second {
return MinWakeupInterval
} else if c.options.WakeupInterval < MinWakeupInterval {
c.options.WakeupInterval = c.options.WakeupInterval * time.Second
c.options.WakeupInterval *= time.Second
}
// Do not run less than once per day.
@@ -786,11 +786,12 @@ func (c *Config) ResolutionLimit() int {
// Disabling or increasing the limit is at your own risk.
// Only sponsors receive support in case of problems.
if result == 0 {
switch {
case result == 0:
return DefaultResolutionLimit
} else if result < 0 {
case result < 0:
return -1
} else if result > 900 {
case result > 900:
result = 900
}
@@ -857,7 +858,7 @@ func (c *Config) initHub() {
c.hubCancel = cancel
c.hubLock.Unlock()
d := 23*time.Hour + time.Duration(float64(2*time.Hour)*rand.Float64())
d := 23*time.Hour + time.Duration(float64(2*time.Hour)*rand.Float64()) //nolint:gosec // jitter for scheduling only, crypto not required
ticker := time.NewTicker(d)
go func() {

View File

@@ -14,7 +14,9 @@ import (
)
const (
// AuthModePublic disables authentication and runs the app in public mode.
AuthModePublic = "public"
// AuthModePasswd enables password-based authentication (default).
AuthModePasswd = "password"
)
@@ -92,7 +94,7 @@ func (c *Config) AdminPassword() string {
} else if fileName := FlagFilePath("ADMIN_PASSWORD"); fileName == "" {
// No password set, this is not an error.
return ""
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 { //nolint:gosec // path is derived from config directory
log.Warnf("config: failed to read admin password from %s (%s)", fileName, err)
return ""
} else {
@@ -111,11 +113,12 @@ func (c *Config) AdminScope() string {
// PasswordLength returns the minimum password length in characters.
func (c *Config) PasswordLength() int {
if c.Public() {
switch {
case c.Public():
return 0
} else if c.options.PasswordLength < 1 {
case c.options.PasswordLength < 1:
return entity.PasswordLengthDefault
} else if c.options.PasswordLength > txt.ClipPassword {
case c.options.PasswordLength > txt.ClipPassword:
return txt.ClipPassword
}
@@ -194,11 +197,12 @@ func (c *Config) SessionTimeout() int64 {
// SessionCache returns the default session cache duration in seconds.
func (c *Config) SessionCache() int64 {
if c.options.SessionCache == 0 {
switch {
case c.options.SessionCache == 0:
return DefaultSessionCache
} else if c.options.SessionCache < 60 {
case c.options.SessionCache < 60:
return 60
} else if c.options.SessionCache > 3600 {
case c.options.SessionCache > 3600:
return 3600
}

View File

@@ -8,8 +8,10 @@ import (
)
const (
// DefaultBackupSchedule defines the default cron schedule for backups.
DefaultBackupSchedule = "daily"
DefaultBackupRetain = 3
// DefaultBackupRetain sets how many backup sets are kept by default.
DefaultBackupRetain = 3
)
// DisableBackups checks if database and album backups as well as YAML sidecar files should not be created.

View File

@@ -6,10 +6,13 @@ import (
gc "github.com/patrickmn/go-cache"
)
// Cache stores shared config values for quick reuse across requests.
var Cache = gc.New(time.Hour, 15*time.Minute)
const (
CacheKeyAppManifest = "app-manifest"
// CacheKeyAppManifest is the cache key for the PWA manifest.
CacheKeyAppManifest = "app-manifest"
// CacheKeyWallpaperUri is the cache key for the current wallpaper URI.
CacheKeyWallpaperUri = "wallpaper-uri"
)

View File

@@ -22,7 +22,11 @@ import (
// DefaultPortalUrl specifies the default portal URL with variable cluster domain.
var DefaultPortalUrl = "https://portal.${PHOTOPRISM_CLUSTER_DOMAIN}"
// DefaultNodeRole is the default node role assigned when none is configured.
var DefaultNodeRole = cluster.RoleApp
// DefaultJWTAllowedScopes lists default OAuth scopes for cluster-issued JWTs.
var DefaultJWTAllowedScopes = "config cluster vision metrics"
// ClusterDomain returns the cluster DOMAIN (lowercase DNS name; 163 chars).
@@ -146,7 +150,7 @@ func (c *Config) JoinToken() string {
}
if fs.FileExistsNotEmpty(fileName) {
if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 { //nolint:gosec // path derived from config directory
log.Warnf("config: could not read cluster join token from %s (%s)", fileName, err)
} else if s := strings.TrimSpace(string(b)); rnd.IsJoinToken(s, false) {
if c.cache != nil {
@@ -355,7 +359,7 @@ func (c *Config) NodeClientSecret() string {
return ""
}
if b, err := os.ReadFile(fileName); err == nil && len(b) > 0 {
if b, err := os.ReadFile(fileName); err == nil && len(b) > 0 { //nolint:gosec // path derived from config directory
// Do not cache the value. Always read from the disk to ensure
// that updates from other processes are observed.
return string(b)
@@ -530,7 +534,7 @@ func (c *Config) SaveClusterUUID(uuid string) error {
var m Values
if fs.FileExists(fileName) {
if b, err := os.ReadFile(fileName); err == nil && len(b) > 0 {
if b, err := os.ReadFile(fileName); err == nil && len(b) > 0 { //nolint:gosec // path derived from config directory
_ = yaml.Unmarshal(b, &m)
}
}
@@ -573,7 +577,7 @@ func (c *Config) SaveNodeUUID(uuid string) error {
var m Values
if fs.FileExists(fileName) {
if b, err := os.ReadFile(fileName); err == nil && len(b) > 0 {
if b, err := os.ReadFile(fileName); err == nil && len(b) > 0 { //nolint:gosec // path derived from config directory
_ = yaml.Unmarshal(b, &m)
}
}

View File

@@ -297,7 +297,7 @@ func TestConfig_Cluster(t *testing.T) {
assert.True(t, rnd.IsJoinToken(token, false))
assert.FileExists(t, tokenFile)
data, readErr := os.ReadFile(tokenFile)
data, readErr := os.ReadFile(tokenFile) //nolint:gosec // test reads file from temp directory
assert.NoError(t, readErr)
assert.Equal(t, token, strings.TrimSpace(string(data)))
})
@@ -311,7 +311,7 @@ func TestConfig_Cluster(t *testing.T) {
assert.NoError(t, err)
assert.FileExists(t, fileName)
data, readErr := os.ReadFile(fileName)
data, readErr := os.ReadFile(fileName) //nolint:gosec // test reads file from temp directory
assert.NoError(t, readErr)
assert.Equal(t, cluster.ExampleClientSecret, strings.TrimSpace(string(data)))
})
@@ -349,7 +349,7 @@ func TestConfig_Cluster(t *testing.T) {
secretDir := filepath.Join(c.NodeConfigPath(), fs.SecretsDir)
assert.NoError(t, os.MkdirAll(secretDir, fs.ModeDir))
assert.NoError(t, os.Chmod(secretDir, 0o500))
assert.NoError(t, os.Chmod(secretDir, 0o500)) //nolint:gosec // making directory intentionally non-writable for fallback test
_, err := c.SaveNodeClientSecret(cluster.ExampleClientSecret)
assert.Error(t, err)
@@ -385,7 +385,7 @@ func TestConfig_Cluster(t *testing.T) {
secretDir := filepath.Join(c.NodeConfigPath(), fs.SecretsDir)
assert.NoError(t, os.MkdirAll(secretDir, fs.ModeDir))
assert.NoError(t, os.Chmod(secretDir, 0o500))
assert.NoError(t, os.Chmod(secretDir, 0o500)) //nolint:gosec // making directory intentionally non-writable for fallback test
_, _, err := c.SaveJoinToken("")
assert.Error(t, err)
@@ -606,7 +606,7 @@ func TestConfig_ClusterUUID_GenerateAndPersist(t *testing.T) {
}
// Verify content persisted to options.yml.
b, err := os.ReadFile(optionsYaml)
b, err := os.ReadFile(optionsYaml) //nolint:gosec // test reads generated options file
assert.NoError(t, err)
var m map[string]any
assert.NoError(t, yaml.Unmarshal(b, &m))

View File

@@ -27,16 +27,18 @@ const ThemeUri = "/_theme"
// DefaultIndexSchedule defines the default indexing schedule in cron format.
const DefaultIndexSchedule = "" // e.g. "0 */3 * * *" for every 3 hours
// DefaultAutoIndexDelay and DefaultAutoImportDelay set the default safety delay duration
// before starting to index/import in the background.
// DefaultAutoIndexDelay sets the default delay (in seconds) before background indexing starts.
const DefaultAutoIndexDelay = 300 // 5 Minutes
// DefaultAutoImportDelay sets the default delay (in seconds) before background imports start (-1 disables).
const DefaultAutoImportDelay = -1 // Disabled
// MinWakeupInterval and MaxWakeupInterval limit the interval duration
// in which the background worker can be invoked.
const MinWakeupInterval = time.Minute // 1 Minute
const MaxWakeupInterval = time.Hour * 24 // 1 Day
// MinWakeupInterval is the minimum allowed interval for the background worker.
const MinWakeupInterval = time.Minute // 1 Minute
// MaxWakeupInterval is the maximum allowed interval for the background worker.
const MaxWakeupInterval = time.Hour * 24 // 1 Day
// DefaultWakeupIntervalSeconds is the default worker interval in seconds.
const DefaultWakeupIntervalSeconds = int(15 * 60) // 15 Minutes
// DefaultWakeupInterval is the default worker interval as a duration.
const DefaultWakeupInterval = time.Second * time.Duration(DefaultWakeupIntervalSeconds)
// MegaByte defines a megabyte in bytes.

View File

@@ -72,12 +72,15 @@ func (c *Config) WallpaperUri() string {
// Complete URI as needed if file path is valid.
if fileName := clean.Path(wallpaperUri); fileName == "" {
return ""
} else if fs.FileExists(path.Join(c.StaticPath(), wallpaperPath, fileName)) {
wallpaperUri = c.StaticAssetUri(path.Join(wallpaperPath, fileName))
} else if fs.FileExists(c.CustomStaticFile(path.Join(wallpaperPath, fileName))) {
wallpaperUri = c.CustomStaticAssetUri(path.Join(wallpaperPath, fileName))
} else {
return ""
switch {
case fs.FileExists(path.Join(c.StaticPath(), wallpaperPath, fileName)):
wallpaperUri = c.StaticAssetUri(path.Join(wallpaperPath, fileName))
case fs.FileExists(c.CustomStaticFile(path.Join(wallpaperPath, fileName))):
wallpaperUri = c.CustomStaticAssetUri(path.Join(wallpaperPath, fileName))
default:
return ""
}
}
// Cache wallpaper URI if not empty.

View File

@@ -11,7 +11,7 @@ import (
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/mysql" // register mysql dialect
_ "github.com/jinzhu/gorm/dialects/sqlite"
"golang.org/x/mod/semver"
@@ -298,7 +298,7 @@ func (c *Config) DatabasePassword() string {
} else if fileName := FlagFilePath("DATABASE_PASSWORD"); fileName == "" {
// No password set, this is not an error.
return ""
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 { //nolint:gosec // path derived from environment variable for DB password
log.Warnf("config: failed to read database password from %s (%s)", fileName, err)
return ""
} else {
@@ -536,11 +536,12 @@ func (c *Config) checkDb(db *gorm.DB) error {
c.dbVersion = clean.Version(res.Value)
if c.dbVersion == "" {
switch {
case c.dbVersion == "":
log.Warnf("config: unknown database server version")
} else if !c.IsDatabaseVersion("v10.0.0") {
case !c.IsDatabaseVersion("v10.0.0"):
return fmt.Errorf("config: MySQL %s is not supported, see https://docs.photoprism.app/getting-started/#databases", c.dbVersion)
} else if !c.IsDatabaseVersion("v10.5.12") {
case !c.IsDatabaseVersion("v10.5.12"):
return fmt.Errorf("config: MariaDB %s is not supported, see https://docs.photoprism.app/getting-started/#databases", c.dbVersion)
}
case SQLite3:
@@ -641,7 +642,7 @@ func (c *Config) connectDb() error {
// ImportSQL imports a file to the currently configured database.
func (c *Config) ImportSQL(filename string) {
contents, err := os.ReadFile(filename)
contents, err := os.ReadFile(filename) //nolint:gosec // import path is provided by trusted caller
if err != nil {
log.Error(err)

View File

@@ -11,7 +11,7 @@ import (
)
// FaceEngine returns the configured face detection engine. When the config is
// nil or the vision subsystem is not initialised it reports `face.EngineNone`
// nil or the vision subsystem is not initialized it reports `face.EngineNone`
// so callers can short-circuit gracefully.
func (c *Config) FaceEngine() string {
if c == nil {

View File

@@ -37,9 +37,9 @@ func TestConfig_FaceEngine(t *testing.T) {
c.options.ModelsPath = tempModels
modelDir := filepath.Join(tempModels, "scrfd")
require.NoError(t, os.MkdirAll(modelDir, 0o755))
require.NoError(t, os.MkdirAll(modelDir, 0o750))
modelFile := filepath.Join(modelDir, face.DefaultONNXModelFilename)
require.NoError(t, os.WriteFile(modelFile, []byte("onnx"), 0o644))
require.NoError(t, os.WriteFile(modelFile, []byte("onnx"), 0o600))
c.options.FaceEngine = face.EngineAuto
assert.Equal(t, face.EngineONNX, c.FaceEngine())

View File

@@ -6,7 +6,10 @@ import (
"github.com/photoprism/photoprism/internal/service/hub/places"
)
// Sponsor indicates whether sponsor or demo features are enabled.
var Sponsor = Env(EnvDemo, EnvSponsor, EnvTest)
// Features represents the current feature tier (community by default).
var Features = Community
// DisableFrontend checks if the web user interface routes should be disabled.

View File

@@ -212,8 +212,3 @@ func withVisionConfig(t *testing.T, cfg *vision.ConfigValues) {
vision.Config = prev
})
}
func cloneVisionModel(m *vision.Model) *vision.Model {
copy := *m
return &copy
}

View File

@@ -105,15 +105,16 @@ func (c *Config) FFmpegPreset() string {
// FFmpegDevice returns the ffmpeg device path for supported hardware encoders (optional).
func (c *Config) FFmpegDevice() string {
if c.options.FFmpegDevice == "" {
switch {
case c.options.FFmpegDevice == "":
return ""
} else if txt.IsUInt(c.options.FFmpegDevice) {
case txt.IsUInt(c.options.FFmpegDevice):
return c.options.FFmpegDevice
} else if fs.DeviceExists(c.options.FFmpegDevice) {
case fs.DeviceExists(c.options.FFmpegDevice):
return c.options.FFmpegDevice
default:
return ""
}
return ""
}
// FFmpegMapVideo returns the video streams to be transcoded as string.
@@ -140,13 +141,14 @@ func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.O
opt := encode.NewVideoOptions(c.FFmpegBin(), encoder, c.FFmpegSize(), c.FFmpegQuality(), c.FFmpegPreset(), c.FFmpegDevice(), c.FFmpegMapVideo(), c.FFmpegMapAudio())
// Check options and return error if invalid.
if opt.Bin == "" {
switch {
case opt.Bin == "":
return opt, fmt.Errorf("ffmpeg is not installed")
} else if c.DisableFFmpeg() {
case c.DisableFFmpeg():
return opt, fmt.Errorf("ffmpeg is disabled")
} else if bitrate == "" {
case bitrate == "":
return opt, fmt.Errorf("bitrate must not be empty")
} else if encoder.String() == "" {
case encoder.String() == "":
return opt, fmt.Errorf("encoder must not be empty")
}

View File

@@ -16,20 +16,25 @@ import (
)
const (
// OidcDefaultProviderName is the default display name for the built-in OIDC provider.
OidcDefaultProviderName = "OpenID"
// OidcDefaultProviderIcon is the default icon path for the built-in OIDC provider.
OidcDefaultProviderIcon = "img/oidc.svg"
OidcLoginUri = ApiUri + "/oidc/login"
OidcRedirectUri = ApiUri + "/oidc/redirect"
// OidcLoginUri is the login endpoint path for OIDC.
OidcLoginUri = ApiUri + "/oidc/login"
// OidcRedirectUri is the callback endpoint path for OIDC.
OidcRedirectUri = ApiUri + "/oidc/redirect"
)
// OIDCEnabled checks if sign-on via OpenID Connect (OIDC) is fully configured and enabled.
func (c *Config) OIDCEnabled() bool {
if c.options.DisableOIDC {
switch {
case c.options.DisableOIDC:
return false
} else if !c.SiteHttps() {
case !c.SiteHttps():
// Site URL must start with "https://".
return false
} else if !strings.HasPrefix(c.options.OIDCUri, "https://") {
case !strings.HasPrefix(c.options.OIDCUri, "https://"):
// OIDC provider URI must start with "https://".
return false
}
@@ -65,7 +70,7 @@ func (c *Config) OIDCSecret() string {
} else if fileName := FlagFilePath("OIDC_SECRET"); fileName == "" {
// No secret set, this is not an error.
return ""
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 { //nolint:gosec // path derived from config directory
log.Warnf("config: failed to read OIDC client secret from %s (%s)", fileName, err)
return ""
} else {

View File

@@ -14,7 +14,9 @@ import (
)
const (
HttpModeProd = "release"
// HttpModeProd selects Gin's release mode.
HttpModeProd = "release"
// HttpModeDebug selects Gin's debug mode.
HttpModeDebug = "debug"
)
@@ -237,9 +239,7 @@ func (c *Config) TemplateFiles() []string {
continue
}
for _, tmplName := range matches {
results = append(results, tmplName)
}
results = append(results, matches...)
}
return results

View File

@@ -199,7 +199,7 @@ func (c *Config) RobotsTxt() ([]byte, error) {
} else if fileName := filepath.Join(c.ConfigPath(), "robots.txt"); !fs.FileExists(fileName) {
// Do not allow indexing if config/robots.txt does not exist.
return robotsTxt, nil
} else if robots, robotsErr := os.ReadFile(fileName); robotsErr != nil {
} else if robots, robotsErr := os.ReadFile(fileName); robotsErr != nil { //nolint:gosec // robots file path derived from config directory
// Log error and do not allow indexing if config/robots.txt cannot be read.
log.Debugf("config: failed to read robots.txt file (%s)", clean.Error(robotsErr))
return robotsTxt, robotsErr

View File

@@ -52,12 +52,13 @@ func (c *Config) ThumbLibrary() string {
default:
c.options.ThumbLibrary = clean.TypeLowerUnderscore(c.options.ThumbLibrary)
if c.options.ThumbLibrary == "imagine" || c.options.ThumbLibrary == "" {
switch c.options.ThumbLibrary {
case "imagine", "":
c.options.ThumbLibrary = thumb.LibImaging
return thumb.LibImaging
} else if c.options.ThumbLibrary == "vips" || c.options.ThumbLibrary == "libvips" {
case "vips", "libvips":
c.options.ThumbLibrary = thumb.LibVips
} else {
default:
c.options.ThumbLibrary = thumb.LibAuto
}

View File

@@ -8,7 +8,9 @@ import (
)
const (
// PrivateKeyExt is the default file extension for TLS private keys.
PrivateKeyExt = ".key"
// PublicCertExt is the default file extension for TLS certificates.
PublicCertExt = ".crt"
)

View File

@@ -25,7 +25,7 @@ func TestConfig_Usage(t *testing.T) {
t.Logf("Storage Used: %d MB (%d%%), Free: %d MB (%d%%), Total %d MB", result2.FilesUsed/duf.MB, result2.FilesUsedPct, result2.FilesFree/duf.MB, result2.FilesFreePct, result2.FilesTotal/duf.MB)
//result cached
// result cached
assert.GreaterOrEqual(t, result2.FilesUsed, uint64(60000000))
assert.GreaterOrEqual(t, result2.FilesTotal, uint64(60000000))

View File

@@ -104,7 +104,7 @@ func (c *Config) VisionKey() string {
} else if fileName := FlagFilePath("VISION_KEY"); fileName == "" {
// No access token set, this is not an error.
return ""
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 { //nolint:gosec // path derived from config directory
log.Warnf("config: failed to read vision key from %s (%s)", fileName, err)
return ""
} else {

View File

@@ -1,5 +1,4 @@
//go:build develop
// +build develop
package config

View File

@@ -5,9 +5,11 @@ import (
)
var (
// ErrReadOnly indicates an action is not permitted in read-only mode.
ErrReadOnly = errors.New("not available in read-only mode")
)
// LogErr logs a config-related error if it is non-nil.
func LogErr(err error) {
if err != nil {
log.Errorf("config: %s", err.Error())

View File

@@ -23,9 +23,6 @@ func resetExtensionsForTest(t *testing.T) func() {
bootExtensions.Store(Extensions{})
bootMutex.Unlock()
originalExtOnce := extInit
originalBootOnce := bootInit
extInit = sync.Once{}
bootInit = sync.Once{}
@@ -38,8 +35,8 @@ func resetExtensionsForTest(t *testing.T) func() {
bootExtensions.Store(savedBoot)
bootMutex.Unlock()
extInit = originalExtOnce
bootInit = originalBootOnce
extInit = sync.Once{}
bootInit = sync.Once{}
}
}

View File

@@ -1,8 +1,12 @@
package config
var (
SignUpURL = "https://www.photoprism.app/membership"
// SignUpURL points to the membership sign-up page.
SignUpURL = "https://www.photoprism.app/membership"
// MsgSponsor is the default sponsorship message shown to users.
MsgSponsor = "Become a member today, support our mission and enjoy our member benefits! 💎"
MsgSignUp = "Visit " + SignUpURL + " to learn more."
SignUp = Values{"message": MsgSponsor, "url": SignUpURL}
// MsgSignUp is the default sign-up helper text.
MsgSignUp = "Visit " + SignUpURL + " to learn more."
// SignUp bundles the sign-up message and URL used in client responses.
SignUp = Values{"message": MsgSponsor, "url": SignUpURL}
)

View File

@@ -1,7 +1,6 @@
package config
import (
"errors"
"fmt"
"net/url"
"os"
@@ -333,10 +332,10 @@ func (o *Options) Load(fileName string) error {
}
if !fs.FileExists(fileName) {
return errors.New(fmt.Sprintf("%s not found", fileName))
return fmt.Errorf("%s not found", fileName)
}
yamlConfig, err := os.ReadFile(fileName)
yamlConfig, err := os.ReadFile(fileName) //nolint:gosec // configuration file path provided by user/config
if err != nil {
return err

View File

@@ -28,9 +28,10 @@ func NewIcons(c Config) Icons {
staticUri := c.StaticUri
appIcon := c.Icon
if appIcon == "" {
switch {
case appIcon == "":
appIcon = "logo"
} else if c.ThemePath != "" && strings.HasPrefix(appIcon, c.ThemeUri) {
case c.ThemePath != "" && strings.HasPrefix(appIcon, c.ThemeUri):
var appIconSize string
var appIconType string
@@ -52,7 +53,7 @@ func NewIcons(c Config) Icons {
Sizes: appIconSize,
Type: appIconType,
}}
} else if strings.Contains(appIcon, "/") {
case strings.Contains(appIcon, "/"):
var appIconType string
switch fs.FileType(filepath.Base(appIcon)) {

View File

@@ -20,7 +20,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
rows = [][]string{
// Authentication.
{"auth-mode", fmt.Sprintf("%s", c.AuthMode())},
{"auth-mode", c.AuthMode()},
{"admin-user", c.AdminUser()},
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
{"admin-scope", c.AdminScope()},
@@ -182,12 +182,12 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"portal-url", c.PortalUrl()},
{"portal-config-path", c.PortalConfigPath()},
{"portal-theme-path", c.PortalThemePath()},
{"join-token", fmt.Sprintf("%s", strings.Repeat("*", utf8.RuneCountInString(c.JoinToken())))},
{"join-token", strings.Repeat("*", utf8.RuneCountInString(c.JoinToken()))},
{"node-name", c.NodeName()},
{"node-role", c.NodeRole()},
{"node-uuid", c.NodeUUID()},
{"node-client-id", c.NodeClientID()},
{"node-client-secret", fmt.Sprintf("%s", strings.Repeat("*", utf8.RuneCountInString(c.NodeClientSecret())))},
{"node-client-secret", strings.Repeat("*", utf8.RuneCountInString(c.NodeClientSecret()))},
{"jwks-url", c.JWKSUrl()},
{"jwks-cache-ttl", fmt.Sprintf("%d", c.JWKSCacheTTL())},
{"jwt-scope", c.JWTAllowedScopes().String()},
@@ -308,19 +308,20 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"face-engine-run", vision.ReportRunType(c.FaceEngineRunType())},
}...)
if faceEngine == face.EngineONNX {
switch faceEngine {
case face.EngineONNX:
rows = append(rows, [][]string{
{"face-engine-threads", fmt.Sprintf("%d", c.FaceEngineThreads())},
{"face-size", fmt.Sprintf("%d", c.FaceSize())},
{"face-score", fmt.Sprintf("%f", c.FaceScore())},
}...)
} else if faceEngine == face.EnginePigo {
case face.EnginePigo:
rows = append(rows, [][]string{
{"face-size", fmt.Sprintf("%d", c.FaceSize())},
{"face-score", fmt.Sprintf("%f", c.FaceScore())},
{"face-angle", fmt.Sprintf("%v", c.FaceAngles())},
}...)
} else {
default:
rows = append(rows, [][]string{
{"face-engine-threads", fmt.Sprintf("%d", c.FaceEngineThreads())},
{"face-size", fmt.Sprintf("%d", c.FaceSize())},

View File

@@ -11,7 +11,9 @@ import (
)
const (
ScheduleDaily = "daily"
// ScheduleDaily represents the keyword for daily schedules.
ScheduleDaily = "daily"
// ScheduleWeekly represents the keyword for weekly schedules.
ScheduleWeekly = "weekly"
)
@@ -26,9 +28,9 @@ func Schedule(s string) string {
switch s {
case ScheduleDaily:
return fmt.Sprintf("%d %d * * *", rand.IntN(60), rand.IntN(24))
return fmt.Sprintf("%d %d * * *", rand.IntN(60), rand.IntN(24)) //nolint:gosec // non-cryptographic randomness for scheduling
case ScheduleWeekly:
return fmt.Sprintf("%d %d * * 0", rand.IntN(60), rand.IntN(24))
return fmt.Sprintf("%d %d * * 0", rand.IntN(60), rand.IntN(24)) //nolint:gosec // non-cryptographic randomness for scheduling
}
// Example: "0 12 * * *" stands for daily at noon.

View File

@@ -14,7 +14,7 @@ import (
gc "github.com/patrickmn/go-cache"
"github.com/urfave/cli/v2"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/mysql" // register mysql dialect
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/photoprism/photoprism/internal/config/customize"
@@ -50,7 +50,7 @@ func testDataPath(assetsPath string) string {
// PkgNameRegexp normalizes database file names by stripping unsupported
// characters from the Go package identifier supplied by tests.
var PkgNameRegexp = regexp.MustCompile("[^a-zA-Z\\-_]+")
var PkgNameRegexp = regexp.MustCompile(`[^a-zA-Z\-_]+`)
// NewTestOptions builds fully-populated Options suited for backend tests. It
// creates an isolated storage directory under storage/testdata (or the
@@ -329,17 +329,17 @@ func NewTestConfig(dbName string) *Config {
s := customize.NewSettings(c.DefaultTheme(), c.DefaultLocale(), c.DefaultTimezone().String())
if err := fs.MkdirAll(c.ConfigPath()); err != nil {
log.Fatalf("config: %s", err.Error())
log.Panicf("config: %s", err.Error())
}
// Save settings next to the test config path, reusing any existing
// `.yaml`/`.yml` variant so the tests mirror production behavior.
if err := s.Save(fs.ConfigFilePath(c.ConfigPath(), "settings", fs.ExtYml)); err != nil {
log.Fatalf("config: %s", err.Error())
log.Panicf("config: %s", err.Error())
}
if err := c.Init(); err != nil {
log.Fatalf("config: %s", err.Error())
log.Panicf("config: %s", err.Error())
}
if err := c.InitializeTestData(); err != nil {

View File

@@ -1,8 +1,12 @@
package ttl
var (
CacheMaxAge Duration = 31536000 // 365 days is the maximum cache time
CacheDefault Duration = 2592000 // 30 days is the default cache time
CacheVideo Duration = 21600 // 6 hours for video streams
CacheCover Duration = 3600 // 1 hour for album cover images
// CacheMaxAge is the maximum cache duration (in seconds).
CacheMaxAge Duration = 31536000 // 365 days is the maximum cache time
// CacheDefault is the default cache duration (in seconds).
CacheDefault Duration = 2592000 // 30 days is the default cache time
// CacheVideo is the cache duration for video streams (in seconds).
CacheVideo Duration = 21600 // 6 hours for video streams
// CacheCover is the cache duration for album cover images (in seconds).
CacheCover Duration = 3600 // 1 hour for album cover images
)

View File

@@ -7,6 +7,8 @@ import (
var (
once sync.Once
initThumbsMutex sync.Mutex
LowMem = false
TotalMem uint64
// LowMem indicates the system has less RAM than the recommended minimum.
LowMem = false
// TotalMem stores the detected system memory in bytes.
TotalMem uint64
)