feat: fully snake case environment variables

This commit is contained in:
Henrique Dias 2025-11-15 11:28:33 +01:00
parent 5479e5465b
commit 2f12539038
No known key found for this signature in database
9 changed files with 121 additions and 44 deletions

35
cmd/cmd_test.go Normal file
View File

@ -0,0 +1,35 @@
package cmd
import (
"testing"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
// TestEnvCollisions ensures that there are no collisions in the produced environment
// variable names for all commands and their flags.
func TestEnvCollisions(t *testing.T) {
testEnvCollisions(t, rootCmd)
}
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
for _, cmd := range cmd.Commands() {
testEnvCollisions(t, cmd)
}
replacements := generateEnvKeyReplacements(cmd)
envVariables := []string{}
for i := range replacements {
if i%2 != 0 {
envVariables = append(envVariables, replacements[i])
}
}
duplicates := lo.FindDuplicates(envVariables)
if len(duplicates) > 0 {
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
}
}

View File

@ -32,9 +32,9 @@ func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("hide-login-button", false, "hide login button from public pages")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.Bool("hideLoginButton", false, "hide login button from public pages")
flags.Bool("createUserDir", false, "generate user's home directory automatically")
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
@ -53,8 +53,8 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with")
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with")
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with")
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with")
}
func getAuthMethod(defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {

View File

@ -25,12 +25,11 @@ override the options.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
err := getUserDefaults(&defaults, true)
if err != nil {
return err
}
authMethod, auther, err := getAuthentication(flags)
authMethod, auther, err := getAuthentication()
if err != nil {
return err
}
@ -38,9 +37,9 @@ override the options.`,
s := &settings.Settings{
Key: generateKey(),
Signup: v.GetBool("signup"),
HideLoginButton: v.GetBool("hide-login-button"),
CreateUserDir: v.GetBool("create-user-dir"),
MinimumPasswordLength: v.GetUint("minimum-password-length"),
HideLoginButton: v.GetBool("hideloginbutton"),
CreateUserDir: v.GetBool("createuserdir"),
MinimumPasswordLength: v.GetUint("minimumpasswordlength"),
Shell: convertCmdStrToCmdArray(v.GetString("shell")),
AuthMethod: authMethod,
Defaults: defaults,
@ -53,12 +52,12 @@ override the options.`,
},
}
s.FileMode, err = getAndParseMode("file-mode")
s.FileMode, err = getAndParseMode("filemode")
if err != nil {
return err
}
s.DirMode, err = getAndParseMode("dir-mode")
s.DirMode, err = getAndParseMode("dirmode")
if err != nil {
return err
}

View File

@ -17,7 +17,6 @@ var configSetCmd = &cobra.Command{
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
if err != nil {
return err
@ -52,7 +51,7 @@ you want to change. Other options will remain unchanged.`,
ser.Port = v.GetString(key)
case "log":
ser.Log = v.GetString(key)
case "hide-login-button":
case "hideloginbutton":
set.HideLoginButton = v.GetBool(key)
case "signup":
set.Signup = v.GetBool(key)
@ -62,9 +61,9 @@ you want to change. Other options will remain unchanged.`,
var shell string
shell = v.GetString(key)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
case "createuserdir":
set.CreateUserDir = v.GetBool(key)
case "minimum-password-length":
case "minimumpasswordlength":
set.MinimumPasswordLength = v.GetUint(key)
case "branding.name":
set.Branding.Name = v.GetString(key)
@ -78,9 +77,9 @@ you want to change. Other options will remain unchanged.`,
set.Branding.DisableUsedPercentage = v.GetBool(key)
case "branding.files":
set.Branding.Files = v.GetString(key)
case "file-mode":
case "filemode":
set.FileMode, err = getAndParseMode(key)
case "dir-mode":
case "dirmode":
set.DirMode, err = getAndParseMode(key)
}

View File

@ -13,7 +13,6 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
@ -37,8 +36,33 @@ import (
var (
cfgFile string
flatNamesMigrations = map[string]string{
"file-mode": "fileMode",
"dir-mode": "dirMode",
"hide-login-button": "hideLoginButton",
"create-user-dir": "createUserDir",
"minimum-password-length": "minimumPasswordLength",
"socket-perm": "socketPerm",
"disable-thumbnails": "disableThumbnails",
"disable-preview-resize": "disablePreviewResize",
"disable-exec": "disableExec",
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
"img-processors": "imageProcessors",
"cache-dir": "cacheDir",
"token-expiration-time": "tokenExpirationTime",
"baseurl": "baseURL",
}
)
func migrateFlagNames(f *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flatNamesMigrations[name]; ok {
name = newName
}
return pflag.NormalizedName(name)
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
@ -56,6 +80,8 @@ func init() {
flags.String("password", "", "hashed password for the first user when using quick config")
addServerFlags(flags)
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
}
func addServerFlags(flags *pflag.FlagSet) {
@ -66,15 +92,15 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count")
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", true, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
flags.StringP("baseURL", "b", "", "base url")
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
flags.String("tokenExpirationTime", "2h", "user session timeout")
flags.Int("imageProcessors", 4, "image processors count")
flags.Bool("disableThumbnails", false, "disable image thumbnails")
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
flags.Bool("disableExec", true, "disables Command Runner feature")
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
}
var rootCmd = &cobra.Command{
@ -89,9 +115,8 @@ it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
All the flags you have available (except "config" for the configuration file),
can be given either through environment variables or configuration files.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
@ -126,17 +151,14 @@ user created with the credentials from options "username" and "password".`,
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
if err != nil {
return err
}
workersCount := v.GetInt("imageprocessors")
if workersCount < 1 {
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
cacheDir, err := cmd.Flags().GetString("cachedir")
if err != nil {
return err
}
@ -169,7 +191,7 @@ user created with the credentials from options "username" and "password".`,
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm")
socketPerm, err := cmd.Flags().GetUint32("socketperm")
if err != nil {
return err
}
@ -311,16 +333,16 @@ func getRunParams(st *storage.Storage) (*settings.Server, error) {
server.Socket = ""
}
disableThumbnails := v.GetBool("disable-thumbnails")
disableThumbnails := v.GetBool("disablethumbnails")
server.EnableThumbnails = !disableThumbnails
disablePreviewResize := v.GetBool("disable-preview-resize")
disablePreviewResize := v.GetBool("disablepreviewresize")
server.ResizePreview = !disablePreviewResize
disableTypeDetectionByHeader := v.GetBool("disable-type-detection-by-header")
disableTypeDetectionByHeader := v.GetBool("disabletypedetectionbyheader")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
disableExec := v.GetBool("disable-exec")
disableExec := v.GetBool("disableexec")
server.EnableExec = !disableExec
if server.EnableExec {
@ -330,7 +352,7 @@ func getRunParams(st *storage.Storage) (*settings.Server, error) {
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
}
if val, set := getStringParamB("token-expiration-time"); set {
if val, set := getStringParamB("tokenexpirationtime"); set {
server.TokenExpirationTime = val
}
@ -479,7 +501,6 @@ func initConfig() {
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError

View File

@ -12,7 +12,9 @@ import (
"strings"
"github.com/asdine/storm/v3"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
yaml "gopkg.in/yaml.v3"
@ -74,8 +76,26 @@ func dbExists(path string) (bool, error) {
return false, err
}
// Generate the replacements for all environment variables. This allows to
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
// option name is branding.disableexternal.
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
replacements := []string{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
oldName := strings.ToUpper(f.Name)
newName := strings.ToUpper(lo.SnakeCase(f.Name))
replacements = append(replacements, oldName, newName)
})
return replacements
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) error {
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
// Bind the flags
err := v.BindPFlags(cmd.Flags())
if err != nil {
panic(err)

1
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archives v0.1.5
github.com/mitchellh/go-homedir v1.1.0
github.com/samber/lo v1.52.0
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.1

2
go.sum
View File

@ -200,6 +200,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=

View File

@ -132,7 +132,7 @@ Or you can use the web interface to manage them via **Settings** → **Global Se
>
> The **command execution** functionality has been disabled for all existent and new installations by default from version v2.33.8 and onwards, due to continuous and known security vulnerabilities. You should only use this feature if you are aware of all of the security risks involved. For more up to date information, consult issue [#5199](https://github.com/filebrowser/filebrowser/issues/5199).
Within File Browser you can toggle the shell (`< >` icon at the top right) and this will open a shell command window at the bottom of the screen. This functionality can be turned on using the environment variable `FB_DISABLE_EXEC=false` or the flag `--disable-exec=false`.
Within File Browser you can toggle the shell (`< >` icon at the top right) and this will open a shell command window at the bottom of the screen. This functionality can be turned on using the environment variable `FB_DISABLE_EXEC=false` or the flag `--disableExec=false`.
By default no commands are available as the command list is empty. To enable commands these need to either be done on a per-user basis (including for the Admin user).