Compare commits

...

3 Commits

Author SHA1 Message Date
Henrique Dias
e5e1b6dee4 chore(release): 2.41.0 2025-07-22 08:28:22 +02:00
Jagadam Dinesh Reddy
1582b8b2cd feat: better error handling for sys kill signals 2025-07-22 08:25:21 +02:00
Vincent Lee
21ad653b7e feat: Allow file and directory creation modes to be configured
The defaults remain the same as before.
For now, the config options are global instead of per-user.
Note also that the BoltDB creation maintains the old default mode of 0640
since it's not really a user-facing filesystem manipulation.
Fixes #5316, #5200
2025-07-22 07:56:52 +02:00
37 changed files with 950 additions and 395 deletions

View File

@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.41.0](https://github.com/filebrowser/filebrowser/compare/v2.40.2...v2.41.0) (2025-07-22)
### Features
* Allow file and directory creation modes to be configured ([21ad653](https://github.com/filebrowser/filebrowser/commit/21ad653b7eb246c0e95ccdc131f8d59267de7818)), closes [#5316](https://github.com/filebrowser/filebrowser/issues/5316) [#5200](https://github.com/filebrowser/filebrowser/issues/5200)
* better error handling for sys kill signals ([1582b8b](https://github.com/filebrowser/filebrowser/commit/1582b8b2cd1c62fa93e60ca9b4e740e940b02e84))
### [2.40.2](https://github.com/filebrowser/filebrowser/compare/v2.40.1...v2.40.2) (2025-07-17)

View File

@@ -1,12 +1,6 @@
package cmd
import (
"log"
)
// Execute executes the commands.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
func Execute() error {
return rootCmd.Execute()
}

View File

@@ -15,13 +15,18 @@ var cmdsAddCmd = &cobra.Command{
Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2),
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = d.store.Settings.Save(s)
checkErr(err)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}

View File

@@ -14,10 +14,15 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event",
Long: `List all commands for each event.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
evt := mustGetString(cmd.Flags(), "event")
if err != nil {
return err
}
evt, err := getString(cmd.Flags(), "event")
if err != nil {
return err
}
if evt == "" {
printEvents(s.Commands)
@@ -27,5 +32,6 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
return nil
}, pythonConfig{}),
}

View File

@@ -35,22 +35,31 @@ including 'index_end'.`,
return nil
},
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
evt := args[0]
i, err := strconv.Atoi(args[1])
checkErr(err)
if err != nil {
return err
}
f := i
if len(args) == 3 {
f, err = strconv.Atoi(args[2])
checkErr(err)
if err != nil {
return err
}
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
checkErr(err)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}

View File

@@ -49,11 +49,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")
}
//nolint:gocyclo
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
if err != nil {
return "", nil, err
}
method := settings.AuthMethod(methodStr)
var defaultAuther map[string]interface{}
if len(defaults) > 0 {
@@ -64,83 +71,124 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
method = def.AuthMethod
case auth.Auther:
ms, err := json.Marshal(def)
checkErr(err)
if err != nil {
return "", nil, err
}
err = json.Unmarshal(ms, &defaultAuther)
checkErr(err)
if err != nil {
return "", nil, err
}
}
}
}
}
var auther auth.Auther
if method == auth.MethodProxyAuth {
header := mustGetString(flags, "auth.header")
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
}
auther = &auth.ProxyAuth{Header: header}
}
if method == auth.MethodNoAuth {
auther = &auth.NoAuth{}
}
if method == auth.MethodJSONAuth {
jsonAuth := &auth.JSONAuth{}
host := mustGetString(flags, "recaptcha.host")
key := mustGetString(flags, "recaptcha.key")
secret := mustGetString(flags, "recaptcha.secret")
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
auther = jsonAuth
}
if method == auth.MethodHookAuth {
command := mustGetString(flags, "auth.command")
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
}
auther = &auth.HookAuth{Command: command}
}
if auther == nil {
panic(errors.ErrInvalidAuthMethod)
}
return method, auther
return method, defaultAuther, nil
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
if err != nil {
return nil, err
}
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
}
return &auth.ProxyAuth{Header: header}, nil
}
func getNoAuth() auth.Auther {
return &auth.NoAuth{}
}
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
if err != nil {
return nil, err
}
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
return jsonAuth, nil
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "auth.command")
if err != nil {
return nil, err
}
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
}
return &auth.HookAuth{Command: command}, nil
}
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
method, defaultAuther, err := getAuthMethod(flags, defaults...)
if err != nil {
return "", nil, err
}
var auther auth.Auther
switch method {
case auth.MethodProxyAuth:
auther, err = getProxyAuth(flags, defaultAuther)
case auth.MethodNoAuth:
auther = getNoAuth()
case auth.MethodJSONAuth:
auther, err = getJSONAuth(flags, defaultAuther)
case auth.MethodHookAuth:
auther, err = getHookAuth(flags, defaultAuther)
default:
return "", nil, errors.ErrInvalidAuthMethod
}
if err != nil {
return "", nil, err
}
return method, auther, nil
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
@@ -170,6 +218,8 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode)
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, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
@@ -186,6 +236,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
checkErr(err)
if err != nil {
return err
}
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}

View File

@@ -13,13 +13,19 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration",
Long: `Prints the configuration.`,
Args: cobra.NoArgs,
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
RunE: python(func(_ *cobra.Command, _ []string, d *pythonData) error {
set, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err)
printSettings(ser, set, auther)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}

View File

@@ -15,15 +15,21 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
settings, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
server, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
auther, err := d.store.Auth.Get(settings.AuthMethod)
checkErr(err)
if err != nil {
return err
}
data := &settingsFile{
Settings: settings,
@@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
}
err = marshal(args[0], data)
checkErr(err)
if err != nil {
return err
}
return nil
}, pythonConfig{}),
}

View File

@@ -34,26 +34,35 @@ database.
The path must be for a json or yaml file.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
var key []byte
var err error
if d.hadDB {
settings, err := d.store.Settings.Get()
checkErr(err)
settings, settingErr := d.store.Settings.Get()
if settingErr != nil {
return settingErr
}
key = settings.Key
} else {
key = generateKey()
}
file := settingsFile{}
err := unmarshal(args[0], &file)
checkErr(err)
err = unmarshal(args[0], &file)
if err != nil {
return err
}
file.Settings.Key = key
err = d.store.Settings.Save(file.Settings)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(file.Server)
checkErr(err)
if err != nil {
return err
}
var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" {
@@ -63,32 +72,51 @@ The path must be for a json or yaml file.`,
}
var auther auth.Auther
var autherErr error
switch file.Settings.AuthMethod {
case auth.MethodJSONAuth:
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
var a interface{}
a, autherErr = getAuther(auth.JSONAuth{}, rawAuther)
auther = a.(*auth.JSONAuth)
case auth.MethodNoAuth:
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
var a interface{}
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
auther = a.(*auth.NoAuth)
case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
var a interface{}
a, autherErr = getAuther(auth.ProxyAuth{}, rawAuther)
auther = a.(*auth.ProxyAuth)
case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
var a interface{}
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
auther = a.(*auth.HookAuth)
default:
checkErr(errors.New("invalid auth method"))
return errors.New("invalid auth method")
}
if autherErr != nil {
return autherErr
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
printSettings(file.Server, file.Settings, auther)
return printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}
func getAuther(sample auth.Auther, data interface{}) interface{} {
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
authType := reflect.TypeOf(sample)
auther := reflect.New(authType).Interface()
bytes, err := json.Marshal(data)
checkErr(err)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &auther)
checkErr(err)
return auther
if err != nil {
return nil, err
}
return auther, nil
}

View File

@@ -22,52 +22,161 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't
override the options.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(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
}
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
}
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
Key: key,
Signup: signup,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"),
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
}
ser := &settings.Server{
Address: mustGetString(flags, "address"),
Socket: mustGetString(flags, "socket"),
Root: mustGetString(flags, "root"),
BaseURL: mustGetString(flags, "baseurl"),
TLSKey: mustGetString(flags, "key"),
TLSCert: mustGetString(flags, "cert"),
Port: mustGetString(flags, "port"),
Log: mustGetString(flags, "log"),
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
err := d.store.Settings.Save(s)
checkErr(err)
s.DirMode, err = getMode(flags, "file-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,
}
err = d.store.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)
return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}

View File

@@ -16,73 +16,105 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL = mustGetString(flags, flag.Name)
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root = mustGetString(flags, flag.Name)
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket = mustGetString(flags, flag.Name)
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert = mustGetString(flags, flag.Name)
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey = mustGetString(flags, flag.Name)
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address = mustGetString(flags, flag.Name)
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port = mustGetString(flags, flag.Name)
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log = mustGetString(flags, flag.Name)
ser.Log, err = getString(flags, flag.Name)
case "signup":
set.Signup = mustGetBool(flags, flag.Name)
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
var shell string
shell, err = getString(flags, flag.Name)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name)
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength = mustGetUint(flags, flag.Name)
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name)
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name)
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name)
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)
}
})
getUserDefaults(flags, &set.Defaults, false)
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)
checkErr(err)
if err != nil {
return err
}
// check if there are new flags for existing auth method
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
printSettings(ser, set, auther)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}

View File

@@ -39,12 +39,19 @@ var docsCmd = &cobra.Command{
Use: "docs",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir)
RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path")
if err != nil {
return err
}
err = generateDocs(rootCmd, dir)
if err != nil {
return err
}
names := []string{}
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
@@ -56,30 +63,38 @@ var docsCmd = &cobra.Command{
names = append(names, info.Name())
return nil
})
if err != nil {
return err
}
checkErr(err)
printToc(names)
return nil
},
}
func generateDocs(cmd *cobra.Command, dir string) {
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
generateDocs(c, dir)
err := generateDocs(c, dir)
if err != nil {
return err
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
checkErr(err)
if err != nil {
return err
}
defer f.Close()
generateMarkdown(cmd, f)
return generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
@@ -108,7 +123,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
checkErr(err)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {

View File

@@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
pwd, err := users.HashPwd(args[0])
checkErr(err)
if err != nil {
return err
}
fmt.Println(pwd)
return nil
},
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"log"
@@ -25,6 +26,7 @@ import (
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
@@ -39,6 +41,7 @@ var (
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
@@ -112,36 +115,48 @@ set FB_DATABASE.
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".`,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
log.Println(cfgFile)
if !d.hadDB {
quickSetup(cmd.Flags(), d)
err := quickSetup(cmd.Flags(), *d)
if err != nil {
return err
}
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err)
if err != nil {
return err
}
if workersCount < 1 {
log.Fatal("Image resize workers count could not be < 1")
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if err != nil {
return err
}
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
log.Fatalf("can't make directory %s: %s", cacheDir, err)
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server := getRunParams(cmd.Flags(), d.store)
server, err := getRunParams(cmd.Flags(), d.store)
if err != nil {
return err
}
setupLog(server.Log)
root, err := filepath.Abs(server.Root)
checkErr(err)
if err != nil {
return err
}
server.Root = root
adr := server.Address + ":" + server.Port
@@ -151,22 +166,34 @@ user created with the credentials from options "username" and "password".`,
switch {
case server.Socket != "":
listener, err = net.Listen("unix", server.Socket)
checkErr(err)
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr(err)
if err != nil {
return err
}
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
checkErr(err)
if err != nil {
return err
}
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
if err != nil {
return err
}
default:
listener, err = net.Listen("tcp", adr)
checkErr(err)
if err != nil {
return err
}
}
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
@@ -175,7 +202,9 @@ user created with the credentials from options "username" and "password".`,
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err)
if err != nil {
return err
}
defer listener.Close()
@@ -194,8 +223,15 @@ user created with the credentials from options "username" and "password".`,
}()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
<-sigc
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) //nolint:mnd
defer shutdownRelease()
@@ -204,13 +240,28 @@ user created with the credentials from options "username" and "password".`,
log.Fatalf("HTTP shutdown error: %v", err)
}
log.Println("Graceful shutdown complete.")
switch sig {
case syscall.SIGHUP:
d.err = fbErrors.ErrSighup
case syscall.SIGINT:
d.err = fbErrors.ErrSigint
case syscall.SIGQUIT:
d.err = fbErrors.ErrSigquit
case syscall.SIGTERM:
d.err = fbErrors.ErrSigTerm
}
return d.err
}, pythonConfig{allowNoDB: true}),
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer()
checkErr(err)
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
@@ -253,7 +304,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
}
if isAddrSet && isSocketSet {
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
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.
@@ -284,7 +335,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server.TokenExpirationTime = val
}
return server
return server, nil
}
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
@@ -363,7 +414,7 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) {
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
log.Println("Performing quick setup")
set := &settings.Settings{
@@ -406,10 +457,14 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
}
if err != nil {
return err
}
checkErr(err)
err = d.store.Settings.Save(set)
checkErr(err)
if err != nil {
return err
}
ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"),
@@ -422,7 +477,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
if err != nil {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
@@ -430,11 +487,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
if password == "" {
var pwd string
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
checkErr(err)
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)
checkErr(err)
if err != nil {
return err
}
} else {
log.Printf("User '%s' initialize wth user-provided password\n", username)
}
@@ -452,14 +513,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.Defaults.Apply(user)
user.Perm.Admin = true
err = d.store.Users.Save(user)
checkErr(err)
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
checkErr(err)
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")

View File

@@ -40,27 +40,29 @@ including 'index_end'.`,
return nil
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
i, err := strconv.Atoi(args[0])
checkErr(err)
if err != nil {
return err
}
f := i
if len(args) == 2 {
f, err = strconv.Atoi(args[1])
checkErr(err)
if err != nil {
return err
}
}
user := func(u *users.User) {
user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr(err)
return d.store.Users.Save(u)
}
global := func(s *settings.Settings) {
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr(err)
return d.store.Settings.Save(s)
}
runRules(d.store, cmd, user, global)
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

View File

@@ -29,41 +29,62 @@ rules.`,
Args: cobra.NoArgs,
}
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags())
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
id, err := getUserIdentifier(cmd.Flags())
if err != nil {
return err
}
if id != nil {
user, err := st.Users.Get("", id)
checkErr(err)
var user *users.User
user, err = st.Users.Get("", id)
if err != nil {
return err
}
if usersFn != nil {
usersFn(user)
err = usersFn(user)
if err != nil {
return err
}
}
printRules(user.Rules, id)
return
return nil
}
s, err := st.Settings.Get()
checkErr(err)
if err != nil {
return err
}
if globalFn != nil {
globalFn(s)
err = globalFn(s)
if err != nil {
return err
}
}
printRules(s.Rules, id)
return nil
}
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
id := mustGetUint(flags, "id")
username := mustGetString(flags, "username")
if id != 0 {
return id
} else if username != "" {
return username
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
if err != nil {
return nil, err
}
return nil
if id != 0 {
return id, nil
} else if username != "" {
return username, nil
}
return nil, nil
}
func printRules(rulez []rules.Rule, id interface{}) {

View File

@@ -21,9 +21,15 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
allow, err := getBool(cmd.Flags(), "allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
if err != nil {
return err
}
exp := args[0]
if regex {
@@ -41,18 +47,16 @@ var rulesAddCmd = &cobra.Command{
rule.Path = exp
}
user := func(u *users.User) {
user := func(u *users.User) error {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr(err)
return d.store.Users.Save(u)
}
global := func(s *settings.Settings) {
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr(err)
return d.store.Settings.Save(s)
}
runRules(d.store, cmd, user, global)
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

View File

@@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
runRules(d.store, cmd, nil, nil)
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
return runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
}

View File

@@ -21,11 +21,20 @@ var upgradeCmd = &cobra.Command{
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config")
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database"))
checkErr(err)
oldDB, err := getString(flags, "old.database")
if err != nil {
return err
}
oldConf, err := getString(flags, "old.config")
if err != nil {
return err
}
db, err := getString(flags, "database")
if err != nil {
return err
}
return importer.Import(oldDB, oldConf, db)
},
}

View File

@@ -79,50 +79,60 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.Bool("singleClick", false, "use single clicks only")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
if err != nil {
return "", err
}
return viewMode
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
}
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope = mustGetString(flags, flag.Name)
defaults.Scope, err = getString(flags, flag.Name)
case "locale":
defaults.Locale = mustGetString(flags, flag.Name)
defaults.Locale, err = getString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
defaults.ViewMode, err = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
defaults.SingleClick, err = getBool(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
defaults.Perm.Admin, err = getBool(flags, flag.Name)
case "perm.execute":
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
defaults.Perm.Execute, err = getBool(flags, flag.Name)
case "perm.create":
defaults.Perm.Create = mustGetBool(flags, flag.Name)
defaults.Perm.Create, err = getBool(flags, flag.Name)
case "perm.rename":
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
defaults.Perm.Rename, err = getBool(flags, flag.Name)
case "perm.modify":
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
defaults.Perm.Modify, err = getBool(flags, flag.Name)
case "perm.delete":
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
defaults.Perm.Delete, err = getBool(flags, flag.Name)
case "perm.share":
defaults.Perm.Share = mustGetBool(flags, flag.Name)
defaults.Perm.Share, err = getBool(flags, flag.Name)
case "perm.download":
defaults.Perm.Download = mustGetBool(flags, flag.Name)
defaults.Perm.Download, err = getBool(flags, flag.Name)
case "commands":
commands, err := flags.GetStringSlice(flag.Name)
checkErr(err)
defaults.Commands = commands
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By = mustGetString(flags, flag.Name)
defaults.Sorting.By, err = getString(flags, flag.Name)
case "sorting.asc":
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
}
if err != nil {
visitErr = err
}
}
@@ -131,4 +141,5 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
}

View File

@@ -16,36 +16,57 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
getUserDefaults(cmd.Flags(), &s.Defaults, false)
if err != nil {
return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
if err != nil {
return err
}
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
checkErr(err)
if err != nil {
return err
}
lockPassword, err := getBool(cmd.Flags(), "lockPassword")
if err != nil {
return err
}
user := &users.User{
Username: args[0],
Password: password,
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
LockPassword: lockPassword,
}
s.Defaults.Apply(user)
servSettings, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
checkErr(err)
if err != nil {
return err
}
user.Scope = userHome
err = d.store.Users.Save(user)
checkErr(err)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}

View File

@@ -14,11 +14,16 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
list, err := d.store.Users.Gets("")
checkErr(err)
if err != nil {
return err
}
err = marshal(args[0], list)
checkErr(err)
if err != nil {
return err
}
return nil
}, pythonConfig{}),
}

View File

@@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
Short: "Find a user by username or id",
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
Args: cobra.ExactArgs(1),
Run: findUsers,
RunE: findUsers,
}
var usersLsCmd = &cobra.Command{
Use: "ls",
Short: "List all users.",
Args: cobra.NoArgs,
Run: findUsers,
RunE: findUsers,
}
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
var (
list []*users.User
user *users.User
@@ -46,6 +46,9 @@ var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
list, err = d.store.Users.Gets("")
}
checkErr(err)
if err != nil {
return err
}
printUsers(list)
return nil
}, pythonConfig{})

View File

@@ -25,34 +25,54 @@ file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
fd, err := os.Open(args[0])
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr(err)
if err != nil {
return err
}
for _, user := range list {
err = user.Clean("")
checkErr(err)
}
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr(err)
err = marshal("users.backup.json", list)
checkErr(err)
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
checkErr(err)
if err != nil {
return err
}
}
overwrite := mustGetBool(cmd.Flags(), "overwrite")
replace, err := getBool(cmd.Flags(), "replace")
if err != nil {
return err
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
if userImportErr != nil {
return userImportErr
}
err = marshal("users.backup.json", list)
if err != nil {
return err
}
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
if err != nil {
return err
}
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
if err != nil {
return err
}
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
@@ -60,7 +80,7 @@ list or set it to 0.`,
// User exists in DB.
if err == nil {
if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
return errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
}
// If the usernames mismatch, check if there is another one in the DB
@@ -68,7 +88,7 @@ list or set it to 0.`,
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}
} else {
@@ -78,8 +98,11 @@ list or set it to 0.`,
}
err = d.store.Users.Save(user)
checkErr(err)
if err != nil {
return err
}
}
return nil
}, pythonConfig{}),
}

View File

@@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
var err error
@@ -25,7 +25,10 @@ var usersRmCmd = &cobra.Command{
err = d.store.Users.Delete(id)
}
checkErr(err)
if err != nil {
return err
}
fmt.Println("user deleted successfully")
return nil
}, pythonConfig{}),
}

View File

@@ -21,14 +21,22 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
password, err := getString(flags, "password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
if err != nil {
return err
}
s, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
var (
user *users.User
@@ -40,7 +48,9 @@ options you want to change.`,
user, err = d.store.Users.Get("", username)
}
checkErr(err)
if err != nil {
return err
}
defaults := settings.UserDefaults{
Scope: user.Scope,
@@ -51,7 +61,10 @@ options you want to change.`,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
@@ -59,7 +72,10 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword = mustGetBool(flags, "lockPassword")
user.LockPassword, err = getBool(flags, "lockPassword")
if err != nil {
return err
}
if newUsername != "" {
user.Username = newUsername
@@ -67,11 +83,16 @@ options you want to change.`,
if password != "" {
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
checkErr(err)
if err != nil {
return err
}
}
err = d.store.Users.Update(user)
checkErr(err)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}

View File

@@ -4,9 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/asdine/storm/v3"
@@ -14,44 +16,57 @@ import (
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
func checkErr(err error) {
const dbPerms = 0640
func returnErr(err error) error {
if err != nil {
log.Fatal(err)
return err
}
return nil
}
func mustGetString(flags *pflag.FlagSet, flag string) string {
func getString(flags *pflag.FlagSet, flag string) (string, error) {
s, err := flags.GetString(flag)
checkErr(err)
return s
return s, returnErr(err)
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
s, err := getString(flags, flag)
if err != nil {
return 0, err
}
b, err := strconv.ParseUint(s, 0, 32)
if err != nil {
return 0, err
}
return fs.FileMode(b), nil
}
func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
b, err := flags.GetBool(flag)
checkErr(err)
return b
return b, returnErr(err)
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag)
checkErr(err)
return b
return b, returnErr(err)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
checkErr(err)
if err != nil {
panic(err)
}
return k
}
type cobraFunc func(cmd *cobra.Command, args []string)
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
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
@@ -61,6 +76,7 @@ type pythonConfig struct {
type pythonData struct {
hadDB bool
store *storage.Storage
err error
}
func dbExists(path string) (bool, error) {
@@ -84,8 +100,8 @@ func dbExists(path string) (bool, error) {
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
return func(cmd *cobra.Command, args []string) error {
data := &pythonData{hadDB: true}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
@@ -106,18 +122,24 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(files.PermFile, nil))
checkErr(err)
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
if err != nil {
return err
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr(err)
fn(cmd, args, data)
if err != nil {
return err
}
return fn(cmd, args, data)
}
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
@@ -135,7 +157,9 @@ func marshal(filename string, data interface{}) error {
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
switch ext := filepath.Ext(filename); ext {

View File

@@ -3,6 +3,15 @@ package errors
import (
"errors"
"fmt"
"os"
"syscall"
)
const (
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
ExitCodeSighup = 128 + int(syscall.SIGHUP)
ExitCodeSigint = 128 + int(syscall.SIGINT)
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
)
var (
@@ -22,6 +31,10 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrSigTerm = errors.New("exit on signal: sigterm")
ErrSighup = errors.New("exit on signal: sighup")
ErrSigint = errors.New("exit on signal: sigint")
ErrSigquit = errors.New("exit on signal: sigquit")
)
type ErrShortPassword struct {
@@ -31,3 +44,44 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
}
// GetExitCode returns the exit code for a given error.
func GetExitCode(err error) int {
if err == nil {
return 0
}
exitCodeMap := map[error]int{
ErrSigTerm: ExitCodeSigTerm,
ErrSighup: ExitCodeSighup,
ErrSigint: ExitCodeSigint,
ErrSigquit: ExitCodeSigquit,
}
for e, code := range exitCodeMap {
if errors.Is(err, e) {
return code
}
}
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
return exitErr.ExitCode()
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
return 1
}
var syscallErr *os.SyscallError
if errors.As(err, &syscallErr) {
return 1
}
var errno syscall.Errno
if errors.As(err, &errno) {
return 1
}
return 1
}

View File

@@ -27,9 +27,6 @@ import (
"github.com/filebrowser/filebrowser/v2/rules"
)
const PermFile = 0640
const PermDir = 0750
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")

View File

@@ -1,6 +1,7 @@
package fileutils
import (
"io/fs"
"os"
"path"
@@ -8,7 +9,7 @@ import (
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
@@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
return os.ErrInvalid
}
info, err := fs.Stat(src)
info, err := afs.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
return CopyDir(afs, src, dst, fileMode, dirMode)
}
return CopyFile(fs, src, dst)
return CopyFile(afs, src, dst, fileMode, dirMode)
}

View File

@@ -2,6 +2,7 @@ package fileutils
import (
"errors"
"io/fs"
"github.com/spf13/afero"
)
@@ -9,20 +10,20 @@ import (
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
srcinfo, err := afs.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
err = afs.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
dir, _ := afs.Open(source)
obs, err := dir.Readdir(-1)
if err != nil {
return err
@@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}

View File

@@ -2,29 +2,28 @@ package fileutils
import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if afs.Rename(src, dst) == nil {
return nil
}
// fallback
err := Copy(fs, src, dst)
err := Copy(afs, src, dst, fileMode, dirMode)
if err != nil {
_ = fs.Remove(dst)
_ = afs.Remove(dst)
return err
}
if err := fs.RemoveAll(src); err != nil {
if err := afs.RemoveAll(src); err != nil {
return err
}
return nil
@@ -32,9 +31,9 @@ func MoveFile(fs afero.Fs, src, dst string) error {
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Open the source file.
src, err := fs.Open(source)
src, err := afs.Open(source)
if err != nil {
return err
}
@@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
}
@@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error {
}
// Copy the mode
info, err := fs.Stat(source)
info, err := afs.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
err = afs.Chmod(dest, info.Mode())
if err != nil {
return err
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"net/url"
@@ -105,7 +106,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, files.PermDir)
err := d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode)
return errToStatus(err), err
}
@@ -134,7 +135,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode)
if writeErr != nil {
return writeErr
}
@@ -171,7 +172,7 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode)
if writeErr != nil {
return writeErr
}
@@ -243,14 +244,14 @@ func checkParent(src, dst string) error {
return nil
}
func addVersionSuffix(source string, fs afero.Fs) string {
func addVersionSuffix(source string, afs afero.Fs) string {
counter := 1
dir, name := path.Split(source)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := fs.Stat(source); err != nil {
if _, err := afs.Stat(source); err != nil {
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
@@ -261,14 +262,14 @@ func addVersionSuffix(source string, fs afero.Fs) string {
return source
}
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
func writeFile(afs afero.Fs, dst string, in io.Reader, fileMode, dirMode fs.FileMode) (os.FileInfo, error) {
dir, _ := path.Split(dst)
err := fs.MkdirAll(dir, files.PermDir)
err := afs.MkdirAll(dir, dirMode)
if err != nil {
return nil, err
}
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
file, err := afs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return nil, err
}
@@ -306,7 +307,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return fbErrors.ErrPermissionDenied
}
return fileutils.Copy(d.user.Fs, src, dst)
return fileutils.Copy(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
case "rename":
if !d.user.Perm.Rename {
return fbErrors.ErrPermissionDenied
@@ -332,7 +333,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return err
}
return fileutils.MoveFile(d.user.Fs, src, dst)
return fileutils.MoveFile(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
default:
return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams)
}

View File

@@ -92,7 +92,7 @@ func tusPostHandler() handleFunc {
case errors.Is(err, afero.ErrFileNotFound):
dirPath := filepath.Dir(r.URL.Path)
if _, statErr := d.user.Fs.Stat(dirPath); os.IsNotExist(statErr) {
if mkdirErr := d.user.Fs.MkdirAll(dirPath, files.PermDir); mkdirErr != nil {
if mkdirErr := d.user.Fs.MkdirAll(dirPath, d.settings.DirMode); mkdirErr != nil {
return http.StatusInternalServerError, err
}
}
@@ -121,7 +121,7 @@ func tusPostHandler() handleFunc {
fileFlags |= os.O_TRUNC
}
openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, files.PermFile)
openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, d.settings.FileMode)
if err != nil {
return errToStatus(err), err
}
@@ -239,7 +239,7 @@ func tusPatchHandler() handleFunc {
)
}
openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, files.PermFile)
openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, d.settings.FileMode)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not open file: %w", err)
}

View File

@@ -1,9 +1,14 @@
package main
import (
"os"
"github.com/filebrowser/filebrowser/v2/cmd"
"github.com/filebrowser/filebrowser/v2/errors"
)
func main() {
cmd.Execute()
if err := cmd.Execute(); err != nil {
os.Exit(errors.GetExitCode(err))
}
}

View File

@@ -2,6 +2,7 @@ package settings
import (
"crypto/rand"
"io/fs"
"log"
"strings"
"time"
@@ -11,6 +12,8 @@ import (
const DefaultUsersHomeBasePath = "/users"
const DefaultMinimumPasswordLength = 12
const DefaultFileMode = 0640
const DefaultDirMode = 0750
// AuthMethod describes an authentication method.
type AuthMethod string
@@ -29,6 +32,8 @@ type Settings struct {
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
FileMode fs.FileMode `json:"fileMode"`
DirMode fs.FileMode `json:"dirMode"`
}
// GetRules implements rules.Provider.

View File

@@ -42,6 +42,12 @@ func (s *Storage) Get() (*Settings, error) {
RetryCount: DefaultTusRetryCount,
}
}
if set.FileMode == 0 {
set.FileMode = DefaultFileMode
}
if set.DirMode == 0 {
set.DirMode = DefaultDirMode
}
return set, nil
}