From 2f12539038916f464166949923546e84e6ac0b51 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 15 Nov 2025 11:28:33 +0100 Subject: [PATCH] feat: fully snake case environment variables --- cmd/cmd_test.go | 35 +++++++++++++++++++ cmd/config.go | 10 +++--- cmd/config_init.go | 13 ++++--- cmd/config_set.go | 11 +++--- cmd/root.go | 71 +++++++++++++++++++++++++-------------- cmd/utils.go | 20 +++++++++++ go.mod | 1 + go.sum | 2 ++ www/docs/configuration.md | 2 +- 9 files changed, 121 insertions(+), 44 deletions(-) create mode 100644 cmd/cmd_test.go diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 00000000..e4b45c47 --- /dev/null +++ b/cmd/cmd_test.go @@ -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) + } +} diff --git a/cmd/config.go b/cmd/config.go index 30d06413..cbf044ed 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -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) { diff --git a/cmd/config_init.go b/cmd/config_init.go index fa198c21..9bfcf28c 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -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 } diff --git a/cmd/config_set.go b/cmd/config_set.go index 26573a3a..fd51bc6f 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -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) } diff --git a/cmd/root.go b/cmd/root.go index a8209805..e8186713 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 diff --git a/cmd/utils.go b/cmd/utils.go index 373d6f7a..5d45a4ed 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -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) diff --git a/go.mod b/go.mod index 724b0c8f..ec113979 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0b374007..fcd74350 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/www/docs/configuration.md b/www/docs/configuration.md index 77f341a4..2c6b72ae 100644 --- a/www/docs/configuration.md +++ b/www/docs/configuration.md @@ -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).