package cmd import ( "context" "crypto/tls" "errors" "fmt" "io" "io/fs" "log" "net" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" lumberjack "gopkg.in/natefinch/lumberjack.v2" "github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/diskcache" "github.com/filebrowser/filebrowser/v2/frontend" fbhttp "github.com/filebrowser/filebrowser/v2/http" "github.com/filebrowser/filebrowser/v2/img" "github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/users" ) var ( 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() { rootCmd.SilenceUsage = true rootCmd.SetGlobalNormalizationFunc(migrateFlagNames) cobra.MousetrapHelpText = "" rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n") // Flags available across the whole program persistent := rootCmd.PersistentFlags() persistent.StringP("config", "c", "", "config file path") persistent.StringP("database", "d", "./filebrowser.db", "database path") // 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") flags.StringP("port", "p", "8080", "port to listen on") flags.StringP("cert", "t", "", "tls certificate") 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.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{ Use: "filebrowser", Short: "A stylish web-based file browser", Long: `File Browser CLI lets you create the database to use with File Browser, manage your users and all the configurations without accessing the web interface. If you've never run File Browser, you'll need to have a database for 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 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 "--config" is not specified, File Browser will look for a configuration file named .filebrowser.{json, toml, yaml, yml} in the following directories: - ./ - $HOME/ - /etc/filebrowser/ The precedence of the configuration values are as follows: - 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 { if !d.databaseExisted { err := quickSetup(*d) if err != nil { return err } } // build img service imgWorkersCount := d.viper.GetInt("imageProcessors") if imgWorkersCount < 1 { return errors.New("image resize workers count could not be < 1") } imageService := img.New(imgWorkersCount) var fileCache diskcache.Interface = diskcache.NewNoOp() 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) } fileCache = diskcache.New(afero.NewOsFs(), cacheDir) } server, err := getServerSettings(d.viper, d.store) if err != nil { return err } setupLog(server.Log) root, err := filepath.Abs(server.Root) if err != nil { return err } server.Root = root adr := server.Address + ":" + server.Port var listener net.Listener switch { case server.Socket != "": listener, err = net.Listen("unix", server.Socket) if err != nil { return err } socketPerm := d.viper.GetUint32("socketPerm") err = os.Chmod(server.Socket, os.FileMode(socketPerm)) if err != nil { return err } case server.TLSKey != "" && server.TLSCert != "": cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) if err != nil { return err } listener, err = tls.Listen("tcp", adr, &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cer}}, ) if err != nil { return err } default: listener, err = net.Listen("tcp", adr) if err != nil { return err } } assetsFs, err := fs.Sub(frontend.Assets(), "dist") if err != nil { panic(err) } handler, err := fbhttp.NewHandler(imageService, fileCache, d.store, server, assetsFs) if err != nil { return err } defer listener.Close() log.Println("Listening on", listener.Addr().String()) srv := &http.Server{ Handler: handler, ReadHeaderTimeout: 60 * time.Second, } go func() { if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) { log.Fatalf("HTTP server error: %v", err) } log.Println("Stopped serving new connections.") }() sigc := make(chan os.Signal, 1) signal.Notify(sigc, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, ) sig := <-sigc log.Println("Got signal:", sig) shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownRelease() if err := srv.Shutdown(shutdownCtx); err != nil { log.Fatalf("HTTP shutdown error: %v", err) } log.Println("Graceful shutdown complete.") return nil }, pythonConfig{allowsNoDatabase: true}), } 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 := vGetStringIsSet(v, "root"); set { server.Root = val } if val, set := vGetStringIsSet(v, "baseURL"); set { server.BaseURL = val } if val, set := vGetStringIsSet(v, "log"); set { server.Log = val } isSocketSet := false isAddrSet := false if val, set := vGetStringIsSet(v, "address"); set { server.Address = val isAddrSet = isAddrSet || set } if val, set := vGetStringIsSet(v, "port"); set { server.Port = val isAddrSet = isAddrSet || set } if val, set := vGetStringIsSet(v, "key"); set { server.TLSKey = val isAddrSet = isAddrSet || set } if val, set := vGetStringIsSet(v, "cert"); set { server.TLSCert = val isAddrSet = isAddrSet || 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") } // Do not use saved Socket if address was manually set. if isAddrSet && server.Socket != "" { server.Socket = "" } 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!") log.Println("WARNING: This feature has known security vulnerabilities and should not") log.Println("WARNING: you fully understand the risks involved. For more information") log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199") } return server, nil } func vGetStringIsSet(v *viper.Viper, key string) (string, bool) { return v.GetString(key), v.IsSet(key) } func setupLog(logMethod string) { switch logMethod { case "stdout": log.SetOutput(os.Stdout) case "stderr": log.SetOutput(os.Stderr) case "": log.SetOutput(io.Discard) default: log.SetOutput(&lumberjack.Logger{ Filename: logMethod, MaxSize: 100, MaxAge: 14, MaxBackups: 10, }) } } func quickSetup(d pythonData) error { log.Println("Performing quick setup") set := &settings.Settings{ Key: generateKey(), Signup: false, HideLoginButton: true, CreateUserDir: false, MinimumPasswordLength: settings.DefaultMinimumPasswordLength, UserHomeBasePath: settings.DefaultUsersHomeBasePath, Defaults: settings.UserDefaults{ Scope: ".", Locale: "en", SingleClick: false, AceEditorTheme: d.viper.GetString("defaults.aceEditorTheme"), Perm: users.Permissions{ Admin: false, Execute: true, Create: true, Rename: true, Modify: true, Delete: true, Share: true, Download: true, }, }, AuthMethod: "", Branding: settings.Branding{}, Tus: settings.Tus{ ChunkSize: settings.DefaultTusChunkSize, RetryCount: settings.DefaultTusRetryCount, }, Commands: nil, Shell: nil, Rules: nil, } var err error if _, noauth := vGetStringIsSet(d.viper, "noauth"); noauth { set.AuthMethod = auth.MethodNoAuth err = d.store.Auth.Save(&auth.NoAuth{}) } else { set.AuthMethod = auth.MethodJSONAuth err = d.store.Auth.Save(&auth.JSONAuth{}) } if err != nil { return err } err = d.store.Settings.Save(set) if err != nil { return err } ser := &settings.Server{ 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) if err != nil { return err } username := d.viper.GetString("username") password := d.viper.GetString("password") if password == "" { var pwd string pwd, err = users.RandomPwd(set.MinimumPasswordLength) if err != nil { return err } log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd) password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength) if err != nil { return err } } else { log.Printf("User '%s' initialize wth user-provided password\n", username) } if username == "" || password == "" { log.Fatal("username and password cannot be empty during quick setup") } user := &users.User{ Username: username, Password: password, LockPassword: false, } set.Defaults.Apply(user) user.Perm.Admin = true return d.store.Users.Save(user) }