diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c25523b..a02ee24a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [2.48.1](https://github.com/filebrowser/filebrowser/compare/v2.48.0...v2.48.1) (2025-11-17) + + +### Bug Fixes + +* options should only override if set ([420adea](https://github.com/filebrowser/filebrowser/commit/420adea7e61a1c182cddd6fb2544a0752e5709f7)) + +## [2.48.0](https://github.com/filebrowser/filebrowser/compare/v2.47.0...v2.48.0) (2025-11-17) + + +### Features + +* consistent flags and environment variables ([#5549](https://github.com/filebrowser/filebrowser/issues/5549)) ([0a0cb80](https://github.com/filebrowser/filebrowser/commit/0a0cb8046fce52f1ff926171b34bcdb7cd39aab3)) + + +### Bug Fixes + +* add tokenExpirationTime to `config init` and troubleshoot docs ([#5546](https://github.com/filebrowser/filebrowser/issues/5546)) ([8c5dc76](https://github.com/filebrowser/filebrowser/commit/8c5dc7641e6f8aadd9e5d5d3b25a2ad9f1ec9a1e)) +* use all available flags in quick setup ([f41585f](https://github.com/filebrowser/filebrowser/commit/f41585f0392d65c08c01ab65b62d3eeb04c03b7d)) + + +### Refactorings + +* reuse logic for config init and set ([89be0b1](https://github.com/filebrowser/filebrowser/commit/89be0b1873527987dd2dddac746e93b8bc684d46)) + ## [2.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16) diff --git a/Taskfile.yml b/Taskfile.yml index 5977dff0..378e3409 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -49,7 +49,10 @@ tasks: cmds: - task: docs:cli:generate - git add www/docs/cli - - "git commit -m 'chore(docs): update CLI documentation'" + - | + if [[ `git status www/docs/cli --porcelain` ]]; then + git commit -m 'chore(docs): update CLI documentation' + fi - task: release:dry-run - task: release:make 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..550ab5c9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -30,12 +30,18 @@ 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") + // 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("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.String("auth.method", string(auth.MethodJSONAuth), "authentication type") flags.String("auth.header", "", "HTTP header for auth.method=proxy") flags.String("auth.command", "", "command for auth.method=hook") @@ -50,17 +56,13 @@ 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.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) @@ -217,10 +222,16 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address) fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert) fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey) + fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime) 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 +242,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 +256,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, "", " ") @@ -252,3 +266,118 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b)) return nil } + +func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) { + errs := []error{} + hasAuth := false + + visit := func(flag *pflag.Flag) { + var err error + + switch flag.Name { + // Server flags from [addServerFlags] + case "address": + ser.Address, err = flags.GetString(flag.Name) + case "log": + 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 = 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 = flags.GetString(flag.Name) + if err == nil { + set.Shell = convertCmdStrToCmdArray(shell) + } + case "fileMode": + set.FileMode, err = getAndParseFileMode(flags, flag.Name) + case "dirMode": + set.DirMode, err = getAndParseFileMode(flags, flag.Name) + case "auth.method": + hasAuth = true + case "branding.name": + set.Branding.Name, err = flags.GetString(flag.Name) + case "branding.theme": + 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 = 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 "tus.chunkSize": + set.Tus.ChunkSize, err = flags.GetUint64(flag.Name) + case "tus.retryCount": + set.Tus.RetryCount, err = flags.GetUint16(flag.Name) + } + + if err != nil { + errs = append(errs, err) + } + } + + if all { + flags.VisitAll(visit) + } else { + flags.Visit(visit) + } + + err := nerrors.Join(errs...) + if err != nil { + return nil, err + } + + err = getUserDefaults(flags, &set.Defaults, all) + if err != nil { + return nil, err + } + + if all { + set.AuthMethod, auther, err = getAuthentication(flags) + if err != nil { + return nil, err + } + } else { + set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther) + if err != nil { + return nil, err + } + } + + return auther, nil +} 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..2787f080 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -23,170 +23,29 @@ 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) + + // Initialize config + s := &settings.Settings{Key: generateKey()} + ser := &settings.Server{} + + // Fill config with options + auther, err := getSettings(flags, s, ser, nil, true) 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 - } - - 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, - }, - } - - s.FileMode, err = getMode(flags, "file-mode") - if err != nil { - return err - } - - s.DirMode, err = getMode(flags, "dir-mode") - if err != nil { - return err - } - - address, err := getString(flags, "address") - if err != nil { - return err - } - - socket, err := getString(flags, "socket") - if err != nil { - return err - } - - root, err := getString(flags, "root") - if err != nil { - return err - } - - baseURL, err := getString(flags, "baseurl") - if err != nil { - return err - } - - tlsKey, err := getString(flags, "key") - if err != nil { - return err - } - - cert, err := getString(flags, "cert") - if err != nil { - return err - } - - port, err := getString(flags, "port") - if err != nil { - return err - } - - log, err := getString(flags, "log") - if err != nil { - return err - } - - ser := &settings.Server{ - Address: address, - Socket: socket, - Root: root, - BaseURL: baseURL, - TLSKey: tlsKey, - TLSCert: cert, - Port: port, - Log: log, - } - + // Save updated config 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 +57,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..d25b6596 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/spf13/pflag" ) func init() { @@ -18,6 +17,8 @@ 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() + + // Read existing config set, err := d.store.Settings.Get() if err != nil { return err @@ -28,94 +29,28 @@ you want to change. Other options will remain unchanged.`, return err } - 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) - case "address": - ser.Address, err = getString(flags, flag.Name) - case "port": - ser.Port, err = getString(flags, flag.Name) - case "log": - ser.Log, err = getString(flags, flag.Name) - case "hide-login-button": - set.HideLoginButton, err = getBool(flags, flag.Name) - case "signup": - set.Signup, err = getBool(flags, flag.Name) - case "auth.method": - hasAuth = true - case "shell": - var shell string - shell, err = getString(flags, flag.Name) - 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 "branding.name": - set.Branding.Name, err = getString(flags, flag.Name) - case "branding.color": - set.Branding.Color, err = getString(flags, 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) - 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) - 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 - } - - err = getUserDefaults(flags, &set.Defaults, false) - if err != nil { - return err - } - - // read the defaults auther, err := d.store.Auth.Get(set.AuthMethod) if err != nil { return err } - // check if there are new flags for existing auth method - set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther) + // Get updated config + auther, err = getSettings(flags, set, ser, auther, false) if err != nil { return err } + // Save updated config err = d.store.Auth.Save(auther) if err != nil { return err } + err = d.store.Settings.Save(set) if err != nil { return err } + err = d.store.Settings.SaveServer(ser) if err != nil { return err diff --git a/cmd/root.go b/cmd/root.go index 0d103b29..6a44e2f3 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,53 +262,73 @@ 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 { - server.Root = val - } - - if val, set := getStringParamB(flags, "baseurl"); set { - server.BaseURL = val - } - - if val, set := getStringParamB(flags, "log"); set { - server.Log = val - } - isSocketSet := false isAddrSet := false - if val, set := getStringParamB(flags, "address"); set { - server.Address = val - isAddrSet = isAddrSet || set + if v.IsSet("address") { + server.Address = v.GetString("address") + isAddrSet = true } - if val, set := getStringParamB(flags, "port"); set { - server.Port = val - isAddrSet = isAddrSet || set + if v.IsSet("log") { + server.Log = v.GetString("log") } - if val, set := getStringParamB(flags, "key"); set { - server.TLSKey = val - isAddrSet = isAddrSet || set + if v.IsSet("port") { + server.Port = v.GetString("port") + isAddrSet = true } - if val, set := getStringParamB(flags, "cert"); set { - server.TLSCert = val - isAddrSet = isAddrSet || set + if v.IsSet("cert") { + server.TLSCert = v.GetString("cert") + isAddrSet = true } - if val, set := getStringParamB(flags, "socket"); set { - server.Socket = val - isSocketSet = isSocketSet || set + if v.IsSet("key") { + server.TLSKey = v.GetString("key") + isAddrSet = true + } + + if v.IsSet("root") { + server.Root = v.GetString("root") + } + + if v.IsSet("socket") { + server.Socket = v.GetString("socket") + isSocketSet = true + } + + if v.IsSet("baseURL") { + server.BaseURL = v.GetString("baseURL") + } + + if v.IsSet("tokenExpirationTime") { + server.TokenExpirationTime = v.GetString("tokenExpirationTime") + } + + if v.IsSet("disableThumbnails") { + server.EnableThumbnails = !v.GetBool("disableThumbnails") + } + + if v.IsSet("disablePreviewResize") { + server.ResizePreview = !v.GetBool("disablePreviewResize") + } + + if v.IsSet("disableTypeDetectionByHeader") { + server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader") + } + + if v.IsSet("disableExec") { + server.EnableExec = !v.GetBool("disableExec") } if isAddrSet && isSocketSet { @@ -299,18 +340,6 @@ 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 - if server.EnableExec { log.Println("WARNING: Command Runner feature enabled!") log.Println("WARNING: This feature has known security vulnerabilities and should not") @@ -318,71 +347,9 @@ 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 setupLog(logMethod string) { switch logMethod { case "stdout": @@ -401,7 +368,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 +382,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 +406,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error { } var err error - if _, noauth := getStringParamB(flags, "noauth"); noauth { + if d.viper.GetBool("noauth") { set.AuthMethod = auth.MethodNoAuth err = d.store.Auth.Save(&auth.NoAuth{}) } else { @@ -456,13 +423,18 @@ 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"), + TokenExpirationTime: d.viper.GetString("tokenExpirationTime"), + EnableThumbnails: !d.viper.GetBool("disableThumbnails"), + ResizePreview: !d.viper.GetBool("disablePreviewResize"), + EnableExec: !d.viper.GetBool("disableExec"), + TypeDetectionByHeader: !d.viper.GetBool("disableTypeDetectionByHeader"), } err = d.store.Settings.SaveServer(ser) @@ -470,8 +442,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 +476,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/docker/common/defaults/settings.json b/docker/common/defaults/settings.json index e787ef87..cf7fb4ee 100644 --- a/docker/common/defaults/settings.json +++ b/docker/common/defaults/settings.json @@ -5,4 +5,4 @@ "log": "stdout", "database": "/database/filebrowser.db", "root": "/srv" -} \ No newline at end of file +} 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= diff --git a/www/docs/cli/filebrowser-config-init.md b/www/docs/cli/filebrowser-config-init.md index 126082d0..6ea484e6 100644 --- a/www/docs/cli/filebrowser-config-init.md +++ b/www/docs/cli/filebrowser-config-init.md @@ -17,63 +17,60 @@ filebrowser config init [flags] ## Options ``` - --aceEditorTheme string ace editor's syntax highlighting theme for users - -a, --address string address to listen on (default "127.0.0.1") - --auth.command string command for auth.method=hook - --auth.header string HTTP header for auth.method=proxy - --auth.method string authentication type (default "json") - -b, --baseurl string base url - --branding.color string set the theme color - --branding.disableExternal disable external links such as GitHub links - --branding.disableUsedPercentage disable used disk percentage graph - --branding.files string path to directory with images and custom styles - --branding.name string replace 'File Browser' by this name - --branding.theme string set the theme - --cache-dir string file cache directory (disabled if empty) - -t, --cert string tls certificate - --commands strings a list of the commands a user can execute - --create-user-dir generate user's home directory automatically - --dateFormat use date format (true for absolute time, false for relative) - --dir-mode string mode bits that new directories are created with (default "0o750") - --disable-exec disables Command Runner feature (default true) - --disable-preview-resize disable resize of image previews - --disable-thumbnails disable image thumbnails - --disable-type-detection-by-header disables type detection by reading file headers - --file-mode string mode bits that new files are created with (default "0o640") - -h, --help help for init - --hide-login-button hide login button from public pages - --hideDotfiles hide dotfiles - --img-processors int image processors count (default 4) - -k, --key string tls key - --locale string locale for users (default "en") - --lockPassword lock password - -l, --log string log output (default "stdout") - --minimum-password-length uint minimum password length for new users (default 12) - --perm.admin admin perm for users - --perm.create create perm for users (default true) - --perm.delete delete perm for users (default true) - --perm.download download perm for users (default true) - --perm.execute execute perm for users (default true) - --perm.modify modify perm for users (default true) - --perm.rename rename perm for users (default true) - --perm.share share perm for users (default true) - -p, --port string port to listen on (default "8080") - --recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com") - --recaptcha.key string ReCaptcha site key - --recaptcha.secret string ReCaptcha secret - -r, --root string root to prepend to relative paths (default ".") - --scope string scope for users (default ".") - --shell string shell command to which other commands should be appended - -s, --signup allow users to signup - --singleClick use single clicks only - --socket string socket to listen to (cannot be used with address, port, cert nor key flags) - --socket-perm uint32 unix socket file permissions (default 438) - --sorting.asc sorting by ascending order - --sorting.by string sorting mode (name, size or modified) (default "name") - --token-expiration-time string user session timeout (default "2h") - --tus.chunkSize uint the tus chunk size (default 10485760) - --tus.retryCount uint16 the tus retry count (default 5) - --viewMode string view mode for users (default "list") + --aceEditorTheme string ace editor's syntax highlighting theme for users + -a, --address string address to listen on (default "127.0.0.1") + --auth.command string command for auth.method=hook + --auth.header string HTTP header for auth.method=proxy + --auth.method string authentication type (default "json") + -b, --baseURL string base url + --branding.color string set the theme color + --branding.disableExternal disable external links such as GitHub links + --branding.disableUsedPercentage disable used disk percentage graph + --branding.files string path to directory with images and custom styles + --branding.name string replace 'File Browser' by this name + --branding.theme string set the theme + -t, --cert string tls certificate + --commands strings a list of the commands a user can execute + --createUserDir generate user's home directory automatically + --dateFormat use date format (true for absolute time, false for relative) + --dirMode string mode bits that new directories are created with (default "0o750") + --disableExec disables Command Runner feature (default true) + --disablePreviewResize disable resize of image previews + --disableThumbnails disable image thumbnails + --disableTypeDetectionByHeader disables type detection by reading file headers + --fileMode string mode bits that new files are created with (default "0o640") + -h, --help help for init + --hideDotfiles hide dotfiles + --hideLoginButton hide login button from public pages + -k, --key string tls key + --locale string locale for users (default "en") + --lockPassword lock password + -l, --log string log output (default "stdout") + --minimumPasswordLength uint minimum password length for new users (default 12) + --perm.admin admin perm for users + --perm.create create perm for users (default true) + --perm.delete delete perm for users (default true) + --perm.download download perm for users (default true) + --perm.execute execute perm for users (default true) + --perm.modify modify perm for users (default true) + --perm.rename rename perm for users (default true) + --perm.share share perm for users (default true) + -p, --port string port to listen on (default "8080") + --recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com") + --recaptcha.key string ReCaptcha site key + --recaptcha.secret string ReCaptcha secret + -r, --root string root to prepend to relative paths (default ".") + --scope string scope for users (default ".") + --shell string shell command to which other commands should be appended + -s, --signup allow users to signup + --singleClick use single clicks only + --socket string socket to listen to (cannot be used with address, port, cert nor key flags) + --sorting.asc sorting by ascending order + --sorting.by string sorting mode (name, size or modified) (default "name") + --tokenExpirationTime string user session timeout (default "2h") + --tus.chunkSize uint the tus chunk size (default 10485760) + --tus.retryCount uint16 the tus retry count (default 5) + --viewMode string view mode for users (default "list") ``` ## Options inherited from parent commands diff --git a/www/docs/cli/filebrowser-config-set.md b/www/docs/cli/filebrowser-config-set.md index 8d8ea8f5..93515ab4 100644 --- a/www/docs/cli/filebrowser-config-set.md +++ b/www/docs/cli/filebrowser-config-set.md @@ -14,63 +14,60 @@ filebrowser config set [flags] ## Options ``` - --aceEditorTheme string ace editor's syntax highlighting theme for users - -a, --address string address to listen on (default "127.0.0.1") - --auth.command string command for auth.method=hook - --auth.header string HTTP header for auth.method=proxy - --auth.method string authentication type (default "json") - -b, --baseurl string base url - --branding.color string set the theme color - --branding.disableExternal disable external links such as GitHub links - --branding.disableUsedPercentage disable used disk percentage graph - --branding.files string path to directory with images and custom styles - --branding.name string replace 'File Browser' by this name - --branding.theme string set the theme - --cache-dir string file cache directory (disabled if empty) - -t, --cert string tls certificate - --commands strings a list of the commands a user can execute - --create-user-dir generate user's home directory automatically - --dateFormat use date format (true for absolute time, false for relative) - --dir-mode string mode bits that new directories are created with (default "0o750") - --disable-exec disables Command Runner feature (default true) - --disable-preview-resize disable resize of image previews - --disable-thumbnails disable image thumbnails - --disable-type-detection-by-header disables type detection by reading file headers - --file-mode string mode bits that new files are created with (default "0o640") - -h, --help help for set - --hide-login-button hide login button from public pages - --hideDotfiles hide dotfiles - --img-processors int image processors count (default 4) - -k, --key string tls key - --locale string locale for users (default "en") - --lockPassword lock password - -l, --log string log output (default "stdout") - --minimum-password-length uint minimum password length for new users (default 12) - --perm.admin admin perm for users - --perm.create create perm for users (default true) - --perm.delete delete perm for users (default true) - --perm.download download perm for users (default true) - --perm.execute execute perm for users (default true) - --perm.modify modify perm for users (default true) - --perm.rename rename perm for users (default true) - --perm.share share perm for users (default true) - -p, --port string port to listen on (default "8080") - --recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com") - --recaptcha.key string ReCaptcha site key - --recaptcha.secret string ReCaptcha secret - -r, --root string root to prepend to relative paths (default ".") - --scope string scope for users (default ".") - --shell string shell command to which other commands should be appended - -s, --signup allow users to signup - --singleClick use single clicks only - --socket string socket to listen to (cannot be used with address, port, cert nor key flags) - --socket-perm uint32 unix socket file permissions (default 438) - --sorting.asc sorting by ascending order - --sorting.by string sorting mode (name, size or modified) (default "name") - --token-expiration-time string user session timeout (default "2h") - --tus.chunkSize uint the tus chunk size (default 10485760) - --tus.retryCount uint16 the tus retry count (default 5) - --viewMode string view mode for users (default "list") + --aceEditorTheme string ace editor's syntax highlighting theme for users + -a, --address string address to listen on (default "127.0.0.1") + --auth.command string command for auth.method=hook + --auth.header string HTTP header for auth.method=proxy + --auth.method string authentication type (default "json") + -b, --baseURL string base url + --branding.color string set the theme color + --branding.disableExternal disable external links such as GitHub links + --branding.disableUsedPercentage disable used disk percentage graph + --branding.files string path to directory with images and custom styles + --branding.name string replace 'File Browser' by this name + --branding.theme string set the theme + -t, --cert string tls certificate + --commands strings a list of the commands a user can execute + --createUserDir generate user's home directory automatically + --dateFormat use date format (true for absolute time, false for relative) + --dirMode string mode bits that new directories are created with (default "0o750") + --disableExec disables Command Runner feature (default true) + --disablePreviewResize disable resize of image previews + --disableThumbnails disable image thumbnails + --disableTypeDetectionByHeader disables type detection by reading file headers + --fileMode string mode bits that new files are created with (default "0o640") + -h, --help help for set + --hideDotfiles hide dotfiles + --hideLoginButton hide login button from public pages + -k, --key string tls key + --locale string locale for users (default "en") + --lockPassword lock password + -l, --log string log output (default "stdout") + --minimumPasswordLength uint minimum password length for new users (default 12) + --perm.admin admin perm for users + --perm.create create perm for users (default true) + --perm.delete delete perm for users (default true) + --perm.download download perm for users (default true) + --perm.execute execute perm for users (default true) + --perm.modify modify perm for users (default true) + --perm.rename rename perm for users (default true) + --perm.share share perm for users (default true) + -p, --port string port to listen on (default "8080") + --recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com") + --recaptcha.key string ReCaptcha site key + --recaptcha.secret string ReCaptcha secret + -r, --root string root to prepend to relative paths (default ".") + --scope string scope for users (default ".") + --shell string shell command to which other commands should be appended + -s, --signup allow users to signup + --singleClick use single clicks only + --socket string socket to listen to (cannot be used with address, port, cert nor key flags) + --sorting.asc sorting by ascending order + --sorting.by string sorting mode (name, size or modified) (default "name") + --tokenExpirationTime string user session timeout (default "2h") + --tus.chunkSize uint the tus chunk size (default 10485760) + --tus.retryCount uint16 the tus retry count (default 5) + --viewMode string view mode for users (default "list") ``` ## Options inherited from parent commands diff --git a/www/docs/cli/filebrowser.md b/www/docs/cli/filebrowser.md index a8cbe669..8383ec97 100644 --- a/www/docs/cli/filebrowser.md +++ b/www/docs/cli/filebrowser.md @@ -13,12 +13,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/ @@ -26,15 +28,11 @@ 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 @@ -47,28 +45,28 @@ filebrowser [flags] ## Options ``` - -a, --address string address to listen on (default "127.0.0.1") - -b, --baseurl string base url - --cache-dir string file cache directory (disabled if empty) - -t, --cert string tls certificate - -c, --config string config file path - -d, --database string database path (default "./filebrowser.db") - --disable-exec disables Command Runner feature (default true) - --disable-preview-resize disable resize of image previews - --disable-thumbnails disable image thumbnails - --disable-type-detection-by-header disables type detection by reading file headers - -h, --help help for filebrowser - --img-processors int image processors count (default 4) - -k, --key string tls key - -l, --log string log output (default "stdout") - --noauth use the noauth auther when using quick setup - --password string hashed password for the first user when using quick config - -p, --port string port to listen on (default "8080") - -r, --root string root to prepend to relative paths (default ".") - --socket string socket to listen to (cannot be used with address, port, cert nor key flags) - --socket-perm uint32 unix socket file permissions (default 438) - --token-expiration-time string user session timeout (default "2h") - --username string username for the first user when using quick config (default "admin") + -a, --address string address to listen on (default "127.0.0.1") + -b, --baseURL string base url + --cacheDir string file cache directory (disabled if empty) + -t, --cert string tls certificate + -c, --config string config file path + -d, --database string database path (default "./filebrowser.db") + --disableExec disables Command Runner feature (default true) + --disablePreviewResize disable resize of image previews + --disableThumbnails disable image thumbnails + --disableTypeDetectionByHeader disables type detection by reading file headers + -h, --help help for filebrowser + --imageProcessors int image processors count (default 4) + -k, --key string tls key + -l, --log string log output (default "stdout") + --noauth use the noauth auther when using quick setup + --password string hashed password for the first user when using quick setup + -p, --port string port to listen on (default "8080") + -r, --root string root to prepend to relative paths (default ".") + --socket string socket to listen to (cannot be used with address, port, cert nor key flags) + --socketPerm uint32 unix socket file permissions (default 438) + --tokenExpirationTime string user session timeout (default "2h") + --username string username for the first user when using quick setup (default "admin") ``` ## See Also diff --git a/www/docs/troubleshooting.md b/www/docs/troubleshooting.md new file mode 100644 index 00000000..a994957e --- /dev/null +++ b/www/docs/troubleshooting.md @@ -0,0 +1,9 @@ +# Troubleshooting + +## Session Timeout + +By default, user sessions expire after **2 hours**. If you're uploading large files over slower connections, you may need to increase this timeout to prevent sessions from expiring mid-upload. You can configure the session timeout using the `tokenExpirationTime` setting. + +You can either set this option during runtime by using the flag `--tokenExpirationTime`, the environment variable `FB_TOKEN_EXPIRATION_TIME`, or in your configuration file. If you want to persist this to the configuration, please use [`filebrowser config set`](cli/filebrowser-config-set.md). + +Valid duration formats include `"2h"`, `"30m"`, `"24h"`, or combinations like `"2h30m"`. diff --git a/www/mkdocs.yml b/www/mkdocs.yml index 7558c534..c71c6798 100644 --- a/www/mkdocs.yml +++ b/www/mkdocs.yml @@ -100,6 +100,7 @@ nav: - customization.md - authentication.md - command-execution.md + - Troubleshooting: troubleshooting.md - Deployment: deployment.md - Command Line Usage: - cli/filebrowser.md