mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
CI: Apply Go linter recommendations to "internal/config" packages #5330
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
45
internal/config/cli_context_test.go
Normal file
45
internal/config/cli_context_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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; 1–63 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ©
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build develop
|
||||
// +build develop
|
||||
|
||||
package config
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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())},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user