From 0a0cb8046fce52f1ff926171b34bcdb7cd39aab3 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 17 Nov 2025 08:45:43 +0100 Subject: [PATCH] feat: consistent flags and environment variables (#5549) - In the root command, all flags are now correctly available as environmental variables, except for `--config` flag. This was already supposed to be the case, but due to bugs in the implementation it didn't work properly. - All configuration options (unless I missed something) that are available as flags should now properly update the configuration when using the `config init` and `config set` commands. - Flag names are now consistently in the lowerCamelCase format. All flags that were in a different format have been updated in a backwards compatible way. For a transitionary period of at least 6 months, both will work: - `--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` --- cmd/cmd_test.go | 35 ++++++ cmd/cmds_ls.go | 4 +- cmd/config.go | 37 ++++-- cmd/config_import.go | 4 +- cmd/config_init.go | 198 ++++++++++++++---------------- cmd/config_set.go | 99 +++++++++------ cmd/root.go | 283 ++++++++++++++++--------------------------- cmd/rules.go | 5 +- cmd/rules_add.go | 8 +- cmd/users.go | 50 ++++---- cmd/users_add.go | 40 +++--- cmd/users_import.go | 5 +- cmd/users_update.go | 19 +-- cmd/utils.go | 150 +++++++++++++++++------ go.mod | 1 + go.sum | 2 + 16 files changed, 503 insertions(+), 437 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/cmds_ls.go b/cmd/cmds_ls.go index fa901a56..ad700eb7 100644 --- a/cmd/cmds_ls.go +++ b/cmd/cmds_ls.go @@ -19,7 +19,8 @@ var cmdsLsCmd = &cobra.Command{ if err != nil { return err } - evt, err := getString(cmd.Flags(), "event") + + evt, err := cmd.Flags().GetString("event") if err != nil { return err } @@ -32,6 +33,7 @@ var cmdsLsCmd = &cobra.Command{ show["after_"+evt] = s.Commands["after_"+evt] printEvents(show) } + return nil }, pythonConfig{}), } diff --git a/cmd/config.go b/cmd/config.go index 6b739610..73d4faa9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -30,10 +30,11 @@ var configCmd = &cobra.Command{ 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") @@ -50,17 +51,18 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("branding.files", "", "path to directory with images and custom styles") flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links") 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") flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size") flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count") } func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) { - methodStr, err := getString(flags, "auth.method") + methodStr, err := flags.GetString("auth.method") if err != nil { return "", nil, err } @@ -91,7 +93,7 @@ func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.Auth } func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) { - header, err := getString(flags, "auth.header") + header, err := flags.GetString("auth.header") if err != nil { return nil, err } @@ -113,15 +115,17 @@ func getNoAuth() auth.Auther { func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) { jsonAuth := &auth.JSONAuth{} - host, err := getString(flags, "recaptcha.host") + host, err := flags.GetString("recaptcha.host") if err != nil { return nil, err } - key, err := getString(flags, "recaptcha.key") + + key, err := flags.GetString("recaptcha.key") if err != nil { return nil, err } - secret, err := getString(flags, "recaptcha.secret") + + secret, err := flags.GetString("recaptcha.secret") if err != nil { return nil, err } @@ -149,11 +153,10 @@ func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au } func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) { - command, err := getString(flags, "auth.command") + command, err := flags.GetString("auth.command") if err != nil { return nil, err } - if command == "" { command = defaultAuther["command"].(string) } @@ -201,6 +204,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength) fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod) fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " ")) + fmt.Fprintln(w, "\nBranding:") fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name) fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files) @@ -208,6 +212,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage) fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color) fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme) + fmt.Fprintln(w, "\nServer:") fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log) fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port) @@ -218,9 +223,14 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert) fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey) fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec) + fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails) + fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview) + fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader) + fmt.Fprintln(w, "\nTUS:") fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize) fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount) + fmt.Fprintln(w, "\nDefaults:") fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope) fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles) @@ -231,9 +241,11 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode) fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " ")) fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme) + fmt.Fprintf(w, "\tSorting:\n") fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By) fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc) + fmt.Fprintf(w, "\tPermissions:\n") fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin) fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute) @@ -243,6 +255,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete) fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share) fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download) + w.Flush() b, err := json.MarshalIndent(auther, "", " ") diff --git a/cmd/config_import.go b/cmd/config_import.go index 7763517d..63d394d7 100644 --- a/cmd/config_import.go +++ b/cmd/config_import.go @@ -37,7 +37,7 @@ The path must be for a json or yaml file.`, RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error { var key []byte var err error - if d.hadDB { + if d.databaseExisted { settings, settingErr := d.store.Settings.Get() if settingErr != nil { return settingErr @@ -104,7 +104,7 @@ The path must be for a json or yaml file.`, } return printSettings(file.Server, file.Settings, auther) - }, pythonConfig{allowNoDB: true}), + }, pythonConfig{allowsNoDatabase: true}), } func getAuther(sample auth.Auther, data interface{}) (interface{}, error) { diff --git a/cmd/config_init.go b/cmd/config_init.go index 47d02d8a..21cacb33 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/settings" ) @@ -23,170 +24,147 @@ to the defaults when creating new users and you don't override the options.`, Args: cobra.NoArgs, RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error { - defaults := settings.UserDefaults{} flags := cmd.Flags() - err := getUserDefaults(flags, &defaults, true) - if err != nil { - return err - } - authMethod, auther, err := getAuthentication(flags) - if err != nil { - return err - } - - key := generateKey() - - signup, err := getBool(flags, "signup") - if err != nil { - return err - } - - hideLoginButton, err := getBool(flags, "hide-login-button") - if err != nil { - return err - } - - createUserDir, err := getBool(flags, "create-user-dir") - if err != nil { - return err - } - - minLength, err := getUint(flags, "minimum-password-length") - if err != nil { - return err - } - - shell, err := getString(flags, "shell") - if err != nil { - return err - } - - brandingName, err := getString(flags, "branding.name") - if err != nil { - return err - } - - brandingDisableExternal, err := getBool(flags, "branding.disableExternal") - if err != nil { - return err - } - - brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage") - if err != nil { - return err - } - - brandingTheme, err := getString(flags, "branding.theme") - if err != nil { - return err - } - - brandingFiles, err := getString(flags, "branding.files") - if err != nil { - return err - } - - tusChunkSize, err := flags.GetUint64("tus.chunkSize") - if err != nil { - return err - } - - tusRetryCount, err := flags.GetUint16("tus.retryCount") - if err != nil { - return err - } + // General Settings s := &settings.Settings{ - Key: key, - Signup: signup, - HideLoginButton: hideLoginButton, - CreateUserDir: createUserDir, - MinimumPasswordLength: minLength, - Shell: convertCmdStrToCmdArray(shell), - AuthMethod: authMethod, - Defaults: defaults, - Branding: settings.Branding{ - Name: brandingName, - DisableExternal: brandingDisableExternal, - DisableUsedPercentage: brandingDisableUsedPercentage, - Theme: brandingTheme, - Files: brandingFiles, - }, - Tus: settings.Tus{ - ChunkSize: tusChunkSize, - RetryCount: tusRetryCount, - }, + Key: generateKey(), } - s.FileMode, err = getMode(flags, "file-mode") + err := getUserDefaults(flags, &s.Defaults, true) if err != nil { return err } - s.DirMode, err = getMode(flags, "dir-mode") + s.Signup, err = flags.GetBool("signup") if err != nil { return err } - address, err := getString(flags, "address") + s.HideLoginButton, err = flags.GetBool("hideLoginButton") if err != nil { return err } - socket, err := getString(flags, "socket") + s.CreateUserDir, err = flags.GetBool("createUserDir") if err != nil { return err } - root, err := getString(flags, "root") + s.MinimumPasswordLength, err = flags.GetUint("minimumPasswordLength") if err != nil { return err } - baseURL, err := getString(flags, "baseurl") + shell, err := flags.GetString("shell") + if err != nil { + return err + } + s.Shell = convertCmdStrToCmdArray(shell) + + s.FileMode, err = getAndParseFileMode(flags, "fileMode") if err != nil { return err } - tlsKey, err := getString(flags, "key") + s.DirMode, err = getAndParseFileMode(flags, "dirMode") if err != nil { return err } - cert, err := getString(flags, "cert") + s.Branding.Name, err = flags.GetString("branding.name") if err != nil { return err } - port, err := getString(flags, "port") + s.Branding.DisableExternal, err = flags.GetBool("branding.disableExternal") if err != nil { return err } - log, err := getString(flags, "log") + s.Branding.DisableUsedPercentage, err = flags.GetBool("branding.disableUsedPercentage") if err != nil { return err } - ser := &settings.Server{ - Address: address, - Socket: socket, - Root: root, - BaseURL: baseURL, - TLSKey: tlsKey, - TLSCert: cert, - Port: port, - Log: log, + s.Branding.Theme, err = flags.GetString("branding.themes") + if err != nil { + return err + } + + s.Branding.Files, err = flags.GetString("branding.files") + if err != nil { + return err + } + + s.Tus.ChunkSize, err = flags.GetUint64("tus.chunkSize") + if err != nil { + return err + } + + s.Tus.RetryCount, err = flags.GetUint16("tus.retryCount") + if err != nil { + return err + } + + var auther auth.Auther + s.AuthMethod, auther, err = getAuthentication(flags) + if err != nil { + return err + } + + // Server Settings + ser := &settings.Server{} + ser.Address, err = flags.GetString("address") + if err != nil { + return err + } + + ser.Socket, err = flags.GetString("socket") + if err != nil { + return err + } + + ser.Root, err = flags.GetString("root") + if err != nil { + return err + } + + ser.BaseURL, err = flags.GetString("baseURL") + if err != nil { + return err + } + + ser.TLSKey, err = flags.GetString("key") + if err != nil { + return err + } + + ser.TLSCert, err = flags.GetString("cert") + if err != nil { + return err + } + + ser.Port, err = flags.GetString("port") + if err != nil { + return err + } + + ser.Log, err = flags.GetString("log") + if err != nil { + return err } err = d.store.Settings.Save(s) if err != nil { return err } + err = d.store.Settings.SaveServer(ser) if err != nil { return err } + err = d.store.Auth.Save(auther) if err != nil { return err @@ -198,5 +176,5 @@ Now add your first user via 'filebrowser users add' and then you just need to call the main command to boot up the server. `) return printSettings(ser, s, auther) - }, pythonConfig{noDB: true}), + }, pythonConfig{expectsNoDatabase: true}), } diff --git a/cmd/config_set.go b/cmd/config_set.go index c362e2e1..74fae9ea 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -18,6 +18,7 @@ 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 @@ -29,64 +30,86 @@ you want to change. Other options will remain unchanged.`, } hasAuth := false + flags.Visit(func(flag *pflag.Flag) { if err != nil { return } + switch flag.Name { - case "baseurl": - ser.BaseURL, err = getString(flags, flag.Name) - case "root": - ser.Root, err = getString(flags, flag.Name) - case "socket": - ser.Socket, err = getString(flags, flag.Name) - case "cert": - ser.TLSCert, err = getString(flags, flag.Name) - case "key": - ser.TLSKey, err = getString(flags, flag.Name) + // Server flags from [addServerFlags] case "address": - ser.Address, err = getString(flags, flag.Name) - case "port": - ser.Port, err = getString(flags, flag.Name) + ser.Address, err = flags.GetString(flag.Name) case "log": - ser.Log, err = getString(flags, flag.Name) - case "hide-login-button": - set.HideLoginButton, err = getBool(flags, flag.Name) + ser.Log, err = flags.GetString(flag.Name) + case "port": + ser.Port, err = flags.GetString(flag.Name) + case "cert": + ser.TLSCert, err = flags.GetString(flag.Name) + case "key": + ser.TLSKey, err = flags.GetString(flag.Name) + case "root": + ser.Root, err = flags.GetString(flag.Name) + case "socket": + ser.Socket, err = flags.GetString(flag.Name) + case "baseURL": + ser.BaseURL, err = flags.GetString(flag.Name) + case "tokenExpirationTime": + ser.TokenExpirationTime, err = flags.GetString(flag.Name) + case "disableThumbnails": + ser.EnableThumbnails, err = flags.GetBool(flag.Name) + ser.EnableThumbnails = !ser.EnableThumbnails + case "disablePreviewResize": + ser.ResizePreview, err = flags.GetBool(flag.Name) + ser.ResizePreview = !ser.ResizePreview + case "disableExec": + ser.EnableExec, err = flags.GetBool(flag.Name) + ser.EnableExec = !ser.EnableExec + case "disableTypeDetectionByHeader": + ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name) + ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader + + // Settings flags from [addConfigFlags] case "signup": - set.Signup, err = getBool(flags, flag.Name) - case "auth.method": - hasAuth = true + set.Signup, err = flags.GetBool(flag.Name) + case "hideLoginButton": + set.HideLoginButton, err = flags.GetBool(flag.Name) + case "createUserDir": + set.CreateUserDir, err = flags.GetBool(flag.Name) + case "minimumPasswordLength": + set.MinimumPasswordLength, err = flags.GetUint(flag.Name) case "shell": var shell string - shell, err = getString(flags, flag.Name) + shell, err = flags.GetString(flag.Name) + if err != nil { + return + } set.Shell = convertCmdStrToCmdArray(shell) - case "create-user-dir": - set.CreateUserDir, err = getBool(flags, flag.Name) - case "minimum-password-length": - set.MinimumPasswordLength, err = getUint(flags, flag.Name) + case "auth.method": + hasAuth = true case "branding.name": - set.Branding.Name, err = getString(flags, flag.Name) - case "branding.color": - set.Branding.Color, err = getString(flags, flag.Name) + set.Branding.Name, err = flags.GetString(flag.Name) case "branding.theme": - set.Branding.Theme, err = getString(flags, flag.Name) - case "branding.disableExternal": - set.Branding.DisableExternal, err = getBool(flags, flag.Name) - case "branding.disableUsedPercentage": - set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name) + set.Branding.Theme, err = flags.GetString(flag.Name) + case "branding.color": + set.Branding.Color, err = flags.GetString(flag.Name) case "branding.files": - set.Branding.Files, err = getString(flags, flag.Name) - case "file-mode": - set.FileMode, err = getMode(flags, flag.Name) - case "dir-mode": - set.DirMode, err = getMode(flags, flag.Name) + set.Branding.Files, err = flags.GetString(flag.Name) + case "branding.disableExternal": + set.Branding.DisableExternal, err = flags.GetBool(flag.Name) + case "branding.disableUsedPercentage": + set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name) + case "fileMode": + set.FileMode, err = getAndParseFileMode(flags, flag.Name) + case "dirMode": + set.DirMode, err = getAndParseFileMode(flags, flag.Name) case "tus.chunkSize": set.Tus.ChunkSize, err = flags.GetUint64(flag.Name) case "tus.retryCount": set.Tus.RetryCount, err = flags.GetUint16(flag.Name) } - }) + }) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 0d103b29..a20b2346 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,15 +13,13 @@ import ( "os" "os/signal" "path/filepath" - "strings" "syscall" "time" - homedir "github.com/mitchellh/go-homedir" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" - v "github.com/spf13/viper" + "github.com/spf13/viper" lumberjack "gopkg.in/natefinch/lumberjack.v2" "github.com/filebrowser/filebrowser/v2/auth" @@ -35,28 +33,67 @@ import ( ) var ( - cfgFile string + flagNamesMigrations = 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", + } + + warnedFlags = map[string]bool{} ) +// TODO(remove): remove after July 2026. +func migrateFlagNames(f *pflag.FlagSet, name string) pflag.NormalizedName { + if newName, ok := flagNamesMigrations[name]; ok { + + if !warnedFlags[name] { + warnedFlags[name] = true + fmt.Printf("WARNING: Flag --%s has been deprecated, use --%s instead\n", name, newName) + } + + name = newName + } + + return pflag.NormalizedName(name) +} + func init() { - cobra.OnInitialize(initConfig) rootCmd.SilenceUsage = true + rootCmd.SetGlobalNormalizationFunc(migrateFlagNames) + cobra.MousetrapHelpText = "" rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n") - flags := rootCmd.Flags() + // Flags available across the whole program persistent := rootCmd.PersistentFlags() - - persistent.StringVarP(&cfgFile, "config", "c", "", "config file path") + persistent.StringP("config", "c", "", "config file path") persistent.StringP("database", "d", "./filebrowser.db", "database path") - flags.Bool("noauth", false, "use the noauth auther when using quick setup") - flags.String("username", "admin", "username for the first user when using quick config") - flags.String("password", "", "hashed password for the first user when using quick config") + // Runtime flags for the root command + flags := rootCmd.Flags() + flags.Bool("noauth", false, "use the noauth auther when using quick setup") + flags.String("username", "admin", "username for the first user when using quick setup") + flags.String("password", "", "hashed password for the first user when using quick setup") + flags.Uint32("socketPerm", 0666, "unix socket file permissions") + flags.String("cacheDir", "", "file cache directory (disabled if empty)") + flags.Int("imageProcessors", 4, "image processors count") addServerFlags(flags) } +// addServerFlags adds server related flags to the given FlagSet. These flags are available +// in both the root command, config set and config init commands. func addServerFlags(flags *pflag.FlagSet) { flags.StringP("address", "a", "127.0.0.1", "address to listen on") flags.StringP("log", "l", "stdout", "log output") @@ -65,15 +102,12 @@ 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.StringP("baseURL", "b", "", "base url") + flags.String("tokenExpirationTime", "2h", "user session timeout") + 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{ @@ -88,12 +122,14 @@ 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. +For this command, all flags are available as environmental variables, +except for "--config", which specifies the configuration file to use. +The environment variables are prefixed by "FB_" followed by the flag name in +UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available +as FB_DISABLE_PREVIEW_RESIZE. -If you don't set "config", it will look for a configuration file called -.filebrowser.{json, toml, yaml, yml} in the following directories: +If "--config" is not specified, File Browser will look for a configuration +file named .filebrowser.{json, toml, yaml, yml} in the following directories: - ./ - $HOME/ @@ -101,44 +137,32 @@ If you don't set "config", it will look for a configuration file called The precedence of the configuration values are as follows: -- flags -- environment variables -- configuration file -- database values -- defaults - -The environment variables are prefixed by "FB_" followed by the option -name in caps. So to set "database" via an env variable, you should -set FB_DATABASE. +- Flags +- Environment variables +- Configuration file +- Database values +- Defaults Also, if the database path doesn't exist, File Browser will enter into the quick setup mode and a new database will be bootstrapped and a new user created with the credentials from options "username" and "password".`, RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error { - log.Println(cfgFile) - - if !d.hadDB { - err := quickSetup(cmd.Flags(), *d) + if !d.databaseExisted { + err := quickSetup(*d) if err != nil { return err } } // build img service - workersCount, err := cmd.Flags().GetInt("img-processors") - if err != nil { - return err - } - if workersCount < 1 { + imgWorkersCount := d.viper.GetInt("imageProcessors") + if imgWorkersCount < 1 { return errors.New("image resize workers count could not be < 1") } - imgSvc := img.New(workersCount) + imageService := img.New(imgWorkersCount) var fileCache diskcache.Interface = diskcache.NewNoOp() - cacheDir, err := cmd.Flags().GetString("cache-dir") - if err != nil { - return err - } + cacheDir := d.viper.GetString("cacheDir") if cacheDir != "" { if err := os.MkdirAll(cacheDir, 0700); err != nil { return fmt.Errorf("can't make directory %s: %w", cacheDir, err) @@ -146,7 +170,7 @@ user created with the credentials from options "username" and "password".`, fileCache = diskcache.New(afero.NewOsFs(), cacheDir) } - server, err := getRunParams(cmd.Flags(), d.store) + server, err := getServerSettings(d.viper, d.store) if err != nil { return err } @@ -168,10 +192,7 @@ user created with the credentials from options "username" and "password".`, if err != nil { return err } - socketPerm, err := cmd.Flags().GetUint32("socket-perm") - if err != nil { - return err - } + socketPerm := d.viper.GetUint32("socketPerm") err = os.Chmod(server.Socket, os.FileMode(socketPerm)) if err != nil { return err @@ -200,7 +221,7 @@ user created with the credentials from options "username" and "password".`, panic(err) } - handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs) + handler, err := fbhttp.NewHandler(imageService, fileCache, d.store, server, assetsFs) if err != nil { return err } @@ -241,55 +262,59 @@ user created with the credentials from options "username" and "password".`, log.Println("Graceful shutdown complete.") return nil - }, pythonConfig{allowNoDB: true}), + }, pythonConfig{allowsNoDatabase: true}), } -func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) { +func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) { server, err := st.Settings.GetServer() if err != nil { return nil, err } - if val, set := getStringParamB(flags, "root"); set { + if val, set := vGetStringIsSet(v, "root"); set { server.Root = val } - if val, set := getStringParamB(flags, "baseurl"); set { + if val, set := vGetStringIsSet(v, "baseURL"); set { server.BaseURL = val } - if val, set := getStringParamB(flags, "log"); set { + if val, set := vGetStringIsSet(v, "log"); set { server.Log = val } isSocketSet := false isAddrSet := false - if val, set := getStringParamB(flags, "address"); set { + if val, set := vGetStringIsSet(v, "address"); set { server.Address = val isAddrSet = isAddrSet || set } - if val, set := getStringParamB(flags, "port"); set { + if val, set := vGetStringIsSet(v, "port"); set { server.Port = val isAddrSet = isAddrSet || set } - if val, set := getStringParamB(flags, "key"); set { + if val, set := vGetStringIsSet(v, "key"); set { server.TLSKey = val isAddrSet = isAddrSet || set } - if val, set := getStringParamB(flags, "cert"); set { + if val, set := vGetStringIsSet(v, "cert"); set { server.TLSCert = val isAddrSet = isAddrSet || set } - if val, set := getStringParamB(flags, "socket"); set { + if val, set := vGetStringIsSet(v, "socket"); set { server.Socket = val isSocketSet = isSocketSet || set } + if val, set := vGetStringIsSet(v, "tokenExpirationTime"); set { + server.TokenExpirationTime = val + } + if isAddrSet && isSocketSet { return nil, errors.New("--socket flag cannot be used with --address, --port, --key nor --cert") } @@ -299,17 +324,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, server.Socket = "" } - disableThumbnails := getBoolParam(flags, "disable-thumbnails") - server.EnableThumbnails = !disableThumbnails - - disablePreviewResize := getBoolParam(flags, "disable-preview-resize") - server.ResizePreview = !disablePreviewResize - - disableTypeDetectionByHeader := getBoolParam(flags, "disable-type-detection-by-header") - server.TypeDetectionByHeader = !disableTypeDetectionByHeader - - disableExec := getBoolParam(flags, "disable-exec") - server.EnableExec = !disableExec + server.EnableThumbnails = !v.GetBool("disableThumbnails") + server.ResizePreview = !v.GetBool("disablePreviewResize") + server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader") + server.EnableExec = !v.GetBool("disableExec") if server.EnableExec { log.Println("WARNING: Command Runner feature enabled!") @@ -318,69 +336,11 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199") } - if val, set := getStringParamB(flags, "token-expiration-time"); set { - server.TokenExpirationTime = val - } - return server, nil } -// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default -// -// NOTE: we could simply bind the flags to viper and use IsSet. -// Although there is a bug on Viper that always returns true on IsSet -// if a flag is binded. Our alternative way is to manually check -// the flag and then the value from env/config/gotten by viper. -// https://github.com/spf13/viper/pull/331 -func getBoolParamB(flags *pflag.FlagSet, key string) (value, ok bool) { - value, _ = flags.GetBool(key) - - // If set on Flags, use it. - if flags.Changed(key) { - return value, true - } - - // If set through viper (env, config), return it. - if v.IsSet(key) { - return v.GetBool(key), true - } - - // Otherwise use default value on flags. - return value, false -} - -func getBoolParam(flags *pflag.FlagSet, key string) bool { - val, _ := getBoolParamB(flags, key) - return val -} - -// getStringParamB returns a parameter as a string and a boolean to tell if it is different from the default -// -// NOTE: we could simply bind the flags to viper and use IsSet. -// Although there is a bug on Viper that always returns true on IsSet -// if a flag is binded. Our alternative way is to manually check -// the flag and then the value from env/config/gotten by viper. -// https://github.com/spf13/viper/pull/331 -func getStringParamB(flags *pflag.FlagSet, key string) (string, bool) { - value, _ := flags.GetString(key) - - // If set on Flags, use it. - if flags.Changed(key) { - return value, true - } - - // If set through viper (env, config), return it. - if v.IsSet(key) { - return v.GetString(key), true - } - - // Otherwise use default value on flags. - return value, false -} - -func getStringParam(flags *pflag.FlagSet, key string) string { - val, _ := getStringParamB(flags, key) - return val +func vGetStringIsSet(v *viper.Viper, key string) (string, bool) { + return v.GetString(key), v.IsSet(key) } func setupLog(logMethod string) { @@ -401,7 +361,7 @@ func setupLog(logMethod string) { } } -func quickSetup(flags *pflag.FlagSet, d pythonData) error { +func quickSetup(d pythonData) error { log.Println("Performing quick setup") set := &settings.Settings{ @@ -415,7 +375,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error { Scope: ".", Locale: "en", SingleClick: false, - AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"), + AceEditorTheme: d.viper.GetString("defaults.aceEditorTheme"), Perm: users.Permissions{ Admin: false, Execute: true, @@ -439,7 +399,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error { } var err error - if _, noauth := getStringParamB(flags, "noauth"); noauth { + if _, noauth := vGetStringIsSet(d.viper, "noauth"); noauth { set.AuthMethod = auth.MethodNoAuth err = d.store.Auth.Save(&auth.NoAuth{}) } else { @@ -456,13 +416,13 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error { } ser := &settings.Server{ - BaseURL: getStringParam(flags, "baseurl"), - Port: getStringParam(flags, "port"), - Log: getStringParam(flags, "log"), - TLSKey: getStringParam(flags, "key"), - TLSCert: getStringParam(flags, "cert"), - Address: getStringParam(flags, "address"), - Root: getStringParam(flags, "root"), + BaseURL: d.viper.GetString("baseURL"), + Port: d.viper.GetString("port"), + Log: d.viper.GetString("log"), + TLSKey: d.viper.GetString("key"), + TLSCert: d.viper.GetString("cert"), + Address: d.viper.GetString("address"), + Root: d.viper.GetString("root"), } err = d.store.Settings.SaveServer(ser) @@ -470,8 +430,8 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error { return err } - username := getStringParam(flags, "username") - password := getStringParam(flags, "password") + username := d.viper.GetString("username") + password := d.viper.GetString("password") if password == "" { var pwd string @@ -504,32 +464,3 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error { return d.store.Users.Save(user) } - -func initConfig() { - if cfgFile == "" { - home, err := homedir.Dir() - if err != nil { - panic(err) - } - v.AddConfigPath(".") - v.AddConfigPath(home) - v.AddConfigPath("/etc/filebrowser/") - v.SetConfigName(".filebrowser") - } else { - v.SetConfigFile(cfgFile) - } - - v.SetEnvPrefix("FB") - v.AutomaticEnv() - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - - if err := v.ReadInConfig(); err != nil { - var configParseError v.ConfigParseError - if errors.As(err, &configParseError) { - panic(err) - } - cfgFile = "No config file used" - } else { - cfgFile = "Using config file: " + v.ConfigFileUsed() - } -} diff --git a/cmd/rules.go b/cmd/rules.go index ffa5b1ae..bdb1d1cf 100644 --- a/cmd/rules.go +++ b/cmd/rules.go @@ -69,11 +69,12 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) } func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) { - id, err := getUint(flags, "id") + id, err := flags.GetUint("id") if err != nil { return nil, err } - username, err := getString(flags, "username") + + username, err := flags.GetString("username") if err != nil { return nil, err } diff --git a/cmd/rules_add.go b/cmd/rules_add.go index 9d1f0cf9..d58a6987 100644 --- a/cmd/rules_add.go +++ b/cmd/rules_add.go @@ -22,14 +22,18 @@ var rulesAddCmd = &cobra.Command{ Long: `Add a global rule or user rule.`, Args: cobra.ExactArgs(1), RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error { - allow, err := getBool(cmd.Flags(), "allow") + flags := cmd.Flags() + + allow, err := flags.GetBool("allow") if err != nil { return err } - regex, err := getBool(cmd.Flags(), "regex") + + regex, err := flags.GetBool("regex") if err != nil { return err } + exp := args[0] if regex { diff --git a/cmd/users.go b/cmd/users.go index c2e2ce1e..86434a42 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -82,63 +82,64 @@ func addUserFlags(flags *pflag.FlagSet) { flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users") } -func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) { - viewModeStr, err := getString(flags, "viewMode") +func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) { + viewModeStr, err := flags.GetString("viewMode") if err != nil { return "", err } + viewMode := users.ViewMode(viewModeStr) if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode { return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"") } + return viewMode, nil } func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error { - var visitErr error + errs := []error{} + visit := func(flag *pflag.Flag) { - if visitErr != nil { - return - } var err error switch flag.Name { case "scope": - defaults.Scope, err = getString(flags, flag.Name) + defaults.Scope, err = flags.GetString(flag.Name) case "locale": - defaults.Locale, err = getString(flags, flag.Name) + defaults.Locale, err = flags.GetString(flag.Name) case "viewMode": - defaults.ViewMode, err = getViewMode(flags) + defaults.ViewMode, err = getAndParseViewMode(flags) case "singleClick": - defaults.SingleClick, err = getBool(flags, flag.Name) + defaults.SingleClick, err = flags.GetBool(flag.Name) case "aceEditorTheme": - defaults.AceEditorTheme, err = getString(flags, flag.Name) + defaults.AceEditorTheme, err = flags.GetString(flag.Name) case "perm.admin": - defaults.Perm.Admin, err = getBool(flags, flag.Name) + defaults.Perm.Admin, err = flags.GetBool(flag.Name) case "perm.execute": - defaults.Perm.Execute, err = getBool(flags, flag.Name) + defaults.Perm.Execute, err = flags.GetBool(flag.Name) case "perm.create": - defaults.Perm.Create, err = getBool(flags, flag.Name) + defaults.Perm.Create, err = flags.GetBool(flag.Name) case "perm.rename": - defaults.Perm.Rename, err = getBool(flags, flag.Name) + defaults.Perm.Rename, err = flags.GetBool(flag.Name) case "perm.modify": - defaults.Perm.Modify, err = getBool(flags, flag.Name) + defaults.Perm.Modify, err = flags.GetBool(flag.Name) case "perm.delete": - defaults.Perm.Delete, err = getBool(flags, flag.Name) + defaults.Perm.Delete, err = flags.GetBool(flag.Name) case "perm.share": - defaults.Perm.Share, err = getBool(flags, flag.Name) + defaults.Perm.Share, err = flags.GetBool(flag.Name) case "perm.download": - defaults.Perm.Download, err = getBool(flags, flag.Name) + defaults.Perm.Download, err = flags.GetBool(flag.Name) case "commands": defaults.Commands, err = flags.GetStringSlice(flag.Name) case "sorting.by": - defaults.Sorting.By, err = getString(flags, flag.Name) + defaults.Sorting.By, err = flags.GetString(flag.Name) case "sorting.asc": - defaults.Sorting.Asc, err = getBool(flags, flag.Name) + defaults.Sorting.Asc, err = flags.GetBool(flag.Name) case "hideDotfiles": - defaults.HideDotfiles, err = getBool(flags, flag.Name) + defaults.HideDotfiles, err = flags.GetBool(flag.Name) } + if err != nil { - visitErr = err + errs = append(errs, err) } } @@ -147,5 +148,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all } else { flags.Visit(visit) } - return visitErr + + return errors.Join(errs...) } diff --git a/cmd/users_add.go b/cmd/users_add.go index dce7ff98..bfc70069 100644 --- a/cmd/users_add.go +++ b/cmd/users_add.go @@ -17,11 +17,12 @@ var usersAddCmd = &cobra.Command{ Long: `Create a new user and add it to the database.`, Args: cobra.ExactArgs(2), RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error { + flags := cmd.Flags() s, err := d.store.Settings.Get() if err != nil { return err } - err = getUserDefaults(cmd.Flags(), &s.Defaults, false) + err = getUserDefaults(flags, &s.Defaults, false) if err != nil { return err } @@ -31,27 +32,24 @@ var usersAddCmd = &cobra.Command{ return err } - lockPassword, err := getBool(cmd.Flags(), "lockPassword") - if err != nil { - return err - } - - dateFormat, err := getBool(cmd.Flags(), "dateFormat") - if err != nil { - return err - } - - hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles") - if err != nil { - return err - } - user := &users.User{ - Username: args[0], - Password: password, - LockPassword: lockPassword, - DateFormat: dateFormat, - HideDotfiles: hideDotfiles, + Username: args[0], + Password: password, + } + + user.LockPassword, err = flags.GetBool("lockPassword") + if err != nil { + return err + } + + user.DateFormat, err = flags.GetBool("dateFormat") + if err != nil { + return err + } + + user.HideDotfiles, err = flags.GetBool("hideDotfiles") + if err != nil { + return err } s.Defaults.Apply(user) diff --git a/cmd/users_import.go b/cmd/users_import.go index 74353c2c..d08889df 100644 --- a/cmd/users_import.go +++ b/cmd/users_import.go @@ -26,6 +26,7 @@ installation. For that, just don't place their ID on the files list or set it to 0.`, Args: jsonYamlArg, RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error { + flags := cmd.Flags() fd, err := os.Open(args[0]) if err != nil { return err @@ -45,7 +46,7 @@ list or set it to 0.`, } } - replace, err := getBool(cmd.Flags(), "replace") + replace, err := flags.GetBool("replace") if err != nil { return err } @@ -69,7 +70,7 @@ list or set it to 0.`, } } - overwrite, err := getBool(cmd.Flags(), "overwrite") + overwrite, err := flags.GetBool("overwrite") if err != nil { return err } diff --git a/cmd/users_update.go b/cmd/users_update.go index a939e605..59854a81 100644 --- a/cmd/users_update.go +++ b/cmd/users_update.go @@ -22,13 +22,14 @@ var usersUpdateCmd = &cobra.Command{ options you want to change.`, Args: cobra.ExactArgs(1), RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error { - username, id := parseUsernameOrID(args[0]) flags := cmd.Flags() - password, err := getString(flags, "password") + username, id := parseUsernameOrID(args[0]) + password, err := flags.GetString("password") if err != nil { return err } - newUsername, err := getString(flags, "username") + + newUsername, err := flags.GetString("username") if err != nil { return err } @@ -41,13 +42,11 @@ options you want to change.`, var ( user *users.User ) - if id != 0 { user, err = d.store.Users.Get("", id) } else { user, err = d.store.Users.Get("", username) } - if err != nil { return err } @@ -61,10 +60,12 @@ options you want to change.`, Sorting: user.Sorting, Commands: user.Commands, } + err = getUserDefaults(flags, &defaults, false) if err != nil { return err } + user.Scope = defaults.Scope user.Locale = defaults.Locale user.ViewMode = defaults.ViewMode @@ -72,15 +73,17 @@ options you want to change.`, user.Perm = defaults.Perm user.Commands = defaults.Commands user.Sorting = defaults.Sorting - user.LockPassword, err = getBool(flags, "lockPassword") + user.LockPassword, err = flags.GetBool("lockPassword") if err != nil { return err } - user.DateFormat, err = getBool(flags, "dateFormat") + + user.DateFormat, err = flags.GetBool("dateFormat") if err != nil { return err } - user.HideDotfiles, err = getBool(flags, "hideDotfiles") + + user.HideDotfiles, err = flags.GetBool("hideDotfiles") if err != nil { return err } diff --git a/cmd/utils.go b/cmd/utils.go index 3ed5c989..a136db10 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -12,8 +12,11 @@ import ( "strings" "github.com/asdine/storm/v3" + homedir "github.com/mitchellh/go-homedir" + "github.com/samber/lo" "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/spf13/viper" yaml "gopkg.in/yaml.v3" "github.com/filebrowser/filebrowser/v2/settings" @@ -21,32 +24,21 @@ import ( "github.com/filebrowser/filebrowser/v2/storage/bolt" ) -const dbPerms = 0640 +const databasePermissions = 0640 -func getString(flags *pflag.FlagSet, flag string) (string, error) { - return flags.GetString(flag) -} - -func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) { - s, err := getString(flags, flag) +func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) { + mode, err := flags.GetString(name) if err != nil { return 0, err } - b, err := strconv.ParseUint(s, 0, 32) + + b, err := strconv.ParseUint(mode, 0, 32) if err != nil { return 0, err } return fs.FileMode(b), nil } -func getBool(flags *pflag.FlagSet, flag string) (bool, error) { - return flags.GetBool(flag) -} - -func getUint(flags *pflag.FlagSet, flag string) (uint, error) { - return flags.GetUint(flag) -} - func generateKey() []byte { k, err := settings.GenerateKey() if err != nil { @@ -55,19 +47,6 @@ func generateKey() []byte { return k } -type cobraFunc func(cmd *cobra.Command, args []string) error -type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error - -type pythonConfig struct { - noDB bool - allowNoDB bool -} - -type pythonData struct { - hadDB bool - store *storage.Storage -} - func dbExists(path string) (bool, error) { stat, err := os.Stat(path) if err == nil { @@ -88,38 +67,131 @@ 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 initViper(cmd *cobra.Command) (*viper.Viper, error) { + v := viper.New() + + // Get config file from flag + cfgFile, err := cmd.Flags().GetString("config") + if err != nil { + return nil, err + } + + // Configuration file + if cfgFile == "" { + home, err := homedir.Dir() + if err != nil { + return nil, err + } + v.AddConfigPath(".") + v.AddConfigPath(home) + v.AddConfigPath("/etc/filebrowser/") + v.SetConfigName(".filebrowser") + } else { + v.SetConfigFile(cfgFile) + } + + // Environment variables + v.SetEnvPrefix("FB") + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...)) + + // Bind the flags + err = v.BindPFlags(cmd.Flags()) + if err != nil { + return nil, err + } + + // Read in configuration + if err := v.ReadInConfig(); err != nil { + if errors.Is(err, viper.ConfigParseError{}) { + return nil, err + } + + log.Println("No config file used") + } else { + log.Printf("Using config file: %s", v.ConfigFileUsed()) + } + + // Return Viper + return v, nil +} + +type cobraFunc func(cmd *cobra.Command, args []string) error +type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error + +type pythonConfig struct { + expectsNoDatabase bool + allowsNoDatabase bool +} + +type pythonData struct { + databaseExisted bool + viper *viper.Viper + store *storage.Storage +} + func python(fn pythonFunc, cfg pythonConfig) cobraFunc { return func(cmd *cobra.Command, args []string) error { - data := &pythonData{hadDB: true} + v, err := initViper(cmd) + if err != nil { + return err + } + + data := &pythonData{databaseExisted: true} + path := v.GetString("database") + + // Only make the viper instance available to the root command (filebrowser). + // This is to make sure that we don't make the mistake of using it somewhere + // else. + if cmd.Name() == "filebrowser" { + data.viper = v + } - path := getStringParam(cmd.Flags(), "database") absPath, err := filepath.Abs(path) if err != nil { - panic(err) + return err } - exists, err := dbExists(path) + exists, err := dbExists(path) if err != nil { - panic(err) - } else if exists && cfg.noDB { + return err + } else if exists && cfg.expectsNoDatabase { log.Fatal(absPath + " already exists") - } else if !exists && !cfg.noDB && !cfg.allowNoDB { + } else if !exists && !cfg.expectsNoDatabase && !cfg.allowsNoDatabase { log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.") - } else if !exists && !cfg.noDB { + } else if !exists && !cfg.expectsNoDatabase { log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db")) } log.Println("Using database: " + absPath) - data.hadDB = exists - db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil)) + data.databaseExisted = exists + + db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil)) if err != nil { return err } defer db.Close() + data.store, err = bolt.NewStorage(db) if err != nil { return err } + return fn(cmd, args, data) } } diff --git a/go.mod b/go.mod index 47da2619..abaeff87 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 c6b0e41b..551f034e 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,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=