feat: config/users import/export (#613)
Supports json and yaml. Former-commit-id: d36b07953ede1842942b7ab477effeb2e5aa7d5b [formerly 51d0d5691d19e0649935816779a34b1b700e088a] [formerly 342f636293be8e38e7907453a67c67e5e9195c78 [formerly 73b8d2ee7ee73c6785b71b2b4caa2362ad84d989]] Former-commit-id: 8ef6a1563ebe425a15a8229165d2ddb043cefb21 [formerly 01c4ac1d89e0d5c6ed16bb7f23c2bbe62085d6e5] Former-commit-id: 6d197ee1931889571c61ad0920e4352d4b02b264
This commit is contained in:
		
							parent
							
								
									b9acd275a2
								
							
						
					
					
						commit
						802318f903
					
				|  | @ -22,11 +22,11 @@ type jsonCred struct { | ||||||
| 
 | 
 | ||||||
| // JSONAuth is a json implementaion of an Auther.
 | // JSONAuth is a json implementaion of an Auther.
 | ||||||
| type JSONAuth struct { | type JSONAuth struct { | ||||||
| 	ReCaptcha *ReCaptcha | 	ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Auth authenticates the user via a json in content body.
 | // Auth authenticates the user via a json in content body.
 | ||||||
| func (a *JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { | func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { | ||||||
| 	var cred jsonCred | 	var cred jsonCred | ||||||
| 
 | 
 | ||||||
| 	if r.Body == nil { | 	if r.Body == nil { | ||||||
|  |  | ||||||
|  | @ -14,6 +14,6 @@ const MethodNoAuth settings.AuthMethod = "noauth" | ||||||
| type NoAuth struct{} | type NoAuth struct{} | ||||||
| 
 | 
 | ||||||
| // Auth uses authenticates user 1.
 | // Auth uses authenticates user 1.
 | ||||||
| func (a *NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { | func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { | ||||||
| 	return sto.Get(root, 1) | 	return sto.Get(root, 1) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,11 +14,11 @@ const MethodProxyAuth settings.AuthMethod = "proxy" | ||||||
| 
 | 
 | ||||||
| // ProxyAuth is a proxy implementation of an auther.
 | // ProxyAuth is a proxy implementation of an auther.
 | ||||||
| type ProxyAuth struct { | type ProxyAuth struct { | ||||||
| 	Header string | 	Header string `json:"header"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Auth authenticates the user via an HTTP header.
 | // Auth authenticates the user via an HTTP header.
 | ||||||
| func (a *ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { | func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { | ||||||
| 	username := r.Header.Get(a.Header) | 	username := r.Header.Get(a.Header) | ||||||
| 	user, err := sto.Get(root, username) | 	user, err := sto.Get(root, username) | ||||||
| 	if err == errors.ErrNotExist { | 	if err == errors.ErrNotExist { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,30 @@ | ||||||
|  | package cmd | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	configCmd.AddCommand(configExportCmd) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var configExportCmd = &cobra.Command{ | ||||||
|  | 	Use:   "export <filename>", | ||||||
|  | 	Short: "Export the configuration to a file.", | ||||||
|  | 	Args:  jsonYamlArg, | ||||||
|  | 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | ||||||
|  | 		settings, err := d.store.Settings.Get() | ||||||
|  | 		checkErr(err) | ||||||
|  | 
 | ||||||
|  | 		auther, err := d.store.Auth.Get(settings.AuthMethod) | ||||||
|  | 		checkErr(err) | ||||||
|  | 
 | ||||||
|  | 		data := &settingsFile{ | ||||||
|  | 			Settings: settings, | ||||||
|  | 			Auther:   auther, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		err = marshal(args[0], data) | ||||||
|  | 		checkErr(err) | ||||||
|  | 	}, pythonConfig{}), | ||||||
|  | } | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | package cmd | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"reflect" | ||||||
|  | 
 | ||||||
|  | 	"github.com/filebrowser/filebrowser/v2/auth" | ||||||
|  | 	"github.com/filebrowser/filebrowser/v2/settings" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	configCmd.AddCommand(configImportCmd) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type settingsFile struct { | ||||||
|  | 	Settings *settings.Settings `json:"settings"` | ||||||
|  | 	Auther   interface{}        `json:"auther"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var configImportCmd = &cobra.Command{ | ||||||
|  | 	Use: "import <filename>", | ||||||
|  | 	Short: `Import a configuration file. This will replace all the existing | ||||||
|  | configuration. Can be used with or without unexisting databases. | ||||||
|  | If used with a nonexisting database, a key will be generated | ||||||
|  | automatically. Otherwise the key will be kept the same as in the | ||||||
|  | database.`, | ||||||
|  | 	Args: jsonYamlArg, | ||||||
|  | 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | ||||||
|  | 		var key []byte | ||||||
|  | 		if d.hadDB { | ||||||
|  | 			settings, err := d.store.Settings.Get() | ||||||
|  | 			checkErr(err) | ||||||
|  | 			key = settings.Key | ||||||
|  | 		} else { | ||||||
|  | 			key = generateRandomBytes(64) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		file := settingsFile{} | ||||||
|  | 		err := unmarshal(args[0], &file) | ||||||
|  | 		checkErr(err) | ||||||
|  | 
 | ||||||
|  | 		file.Settings.Key = key | ||||||
|  | 		err = d.store.Settings.Save(file.Settings) | ||||||
|  | 		checkErr(err) | ||||||
|  | 
 | ||||||
|  | 		autherInterf := cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{})) | ||||||
|  | 
 | ||||||
|  | 		var auther auth.Auther | ||||||
|  | 		switch file.Settings.AuthMethod { | ||||||
|  | 		case auth.MethodJSONAuth: | ||||||
|  | 			auther = getAuther(auth.JSONAuth{}, autherInterf).(*auth.JSONAuth) | ||||||
|  | 		case auth.MethodNoAuth: | ||||||
|  | 			auther = getAuther(auth.NoAuth{}, autherInterf).(*auth.NoAuth) | ||||||
|  | 		case auth.MethodProxyAuth: | ||||||
|  | 			auther = getAuther(auth.ProxyAuth{}, autherInterf).(*auth.ProxyAuth) | ||||||
|  | 		default: | ||||||
|  | 			checkErr(errors.New("invalid auth method")) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		err = d.store.Auth.Save(auther) | ||||||
|  | 		checkErr(err) | ||||||
|  | 		printSettings(file.Settings, auther) | ||||||
|  | 	}, pythonConfig{allowNoDB: true}), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getAuther(sample auth.Auther, data interface{}) interface{} { | ||||||
|  | 	authType := reflect.TypeOf(sample) | ||||||
|  | 	auther := reflect.New(authType).Interface() | ||||||
|  | 	bytes, err := json.Marshal(data) | ||||||
|  | 	checkErr(err) | ||||||
|  | 	err = json.Unmarshal(bytes, &auther) | ||||||
|  | 	checkErr(err) | ||||||
|  | 	return auther | ||||||
|  | } | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | package cmd | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	usersCmd.AddCommand(usersExportCmd) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var usersExportCmd = &cobra.Command{ | ||||||
|  | 	Use:   "export <filename>", | ||||||
|  | 	Short: "Export all users.", | ||||||
|  | 	Args:  jsonYamlArg, | ||||||
|  | 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | ||||||
|  | 		list, err := d.store.Users.Gets("") | ||||||
|  | 		checkErr(err) | ||||||
|  | 
 | ||||||
|  | 		err = marshal(args[0], list) | ||||||
|  | 		checkErr(err) | ||||||
|  | 	}, pythonConfig{}), | ||||||
|  | } | ||||||
|  | @ -0,0 +1,65 @@ | ||||||
|  | package cmd | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"os" | ||||||
|  | 	"strconv" | ||||||
|  | 
 | ||||||
|  | 	"github.com/filebrowser/filebrowser/v2/users" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	usersCmd.AddCommand(usersImportCmd) | ||||||
|  | 	usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var usersImportCmd = &cobra.Command{ | ||||||
|  | 	Use:   "import <filename>", | ||||||
|  | 	Short: "Import users from a file.", | ||||||
|  | 	Args:  jsonYamlArg, | ||||||
|  | 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | ||||||
|  | 		fd, err := os.Open(args[0]) | ||||||
|  | 		checkErr(err) | ||||||
|  | 		defer fd.Close() | ||||||
|  | 
 | ||||||
|  | 		list := []*users.User{} | ||||||
|  | 		err = unmarshal(args[0], &list) | ||||||
|  | 		checkErr(err) | ||||||
|  | 
 | ||||||
|  | 		for _, user := range list { | ||||||
|  | 			err = user.Clean("") | ||||||
|  | 			checkErr(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		overwrite := mustGetBool(cmd, "overwrite") | ||||||
|  | 
 | ||||||
|  | 		for _, user := range list { | ||||||
|  | 			old, err := d.store.Users.Get("", user.ID) | ||||||
|  | 
 | ||||||
|  | 			// User exists in DB.
 | ||||||
|  | 			if err == nil { | ||||||
|  | 				if !overwrite { | ||||||
|  | 					checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred")) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// If the usernames mismatch, check if there is another one in the DB
 | ||||||
|  | 				// with the new username. If there is, print an error and cancel the
 | ||||||
|  | 				// operation
 | ||||||
|  | 				if user.Username != old.Username { | ||||||
|  | 					conflictuous, err := d.store.Users.Get("", user.Username) | ||||||
|  | 					if err == nil { | ||||||
|  | 						checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID)) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			err = d.store.Users.Save(user) | ||||||
|  | 			checkErr(err) | ||||||
|  | 		} | ||||||
|  | 	}, pythonConfig{}), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func usernameConflictError(username string, original, new uint) error { | ||||||
|  | 	return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original))) | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								cmd/utils.go
								
								
								
								
							
							
						
						
									
										81
									
								
								cmd/utils.go
								
								
								
								
							|  | @ -2,8 +2,12 @@ package cmd | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 
 | 
 | ||||||
| 	"github.com/asdine/storm" | 	"github.com/asdine/storm" | ||||||
| 	"github.com/filebrowser/filebrowser/v2/storage" | 	"github.com/filebrowser/filebrowser/v2/storage" | ||||||
|  | @ -11,6 +15,7 @@ import ( | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	"github.com/spf13/pflag" | 	"github.com/spf13/pflag" | ||||||
| 	v "github.com/spf13/viper" | 	v "github.com/spf13/viper" | ||||||
|  | 	yaml "gopkg.in/yaml.v2" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func vaddP(f *pflag.FlagSet, k, p string, i interface{}, u string) { | func vaddP(f *pflag.FlagSet, k, p string, i interface{}, u string) { | ||||||
|  | @ -39,7 +44,8 @@ func vadd(f *pflag.FlagSet, k string, i interface{}, u string) { | ||||||
| 
 | 
 | ||||||
| func checkErr(err error) { | func checkErr(err error) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		panic(err) | 		fmt.Println(err) | ||||||
|  | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -108,3 +114,76 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc { | ||||||
| 		fn(cmd, args, data) | 		fn(cmd, args, data) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func marshal(filename string, data interface{}) error { | ||||||
|  | 	fd, err := os.Create(filename) | ||||||
|  | 	checkErr(err) | ||||||
|  | 	defer fd.Close() | ||||||
|  | 
 | ||||||
|  | 	switch ext := filepath.Ext(filename); ext { | ||||||
|  | 	case ".json": | ||||||
|  | 		encoder := json.NewEncoder(fd) | ||||||
|  | 		encoder.SetIndent("", "    ") | ||||||
|  | 		return encoder.Encode(data) | ||||||
|  | 	case ".yml", ".yaml": | ||||||
|  | 		encoder := yaml.NewEncoder(fd) | ||||||
|  | 		return encoder.Encode(data) | ||||||
|  | 	default: | ||||||
|  | 		return errors.New("invalid format: " + ext) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func unmarshal(filename string, data interface{}) error { | ||||||
|  | 	fd, err := os.Open(filename) | ||||||
|  | 	checkErr(err) | ||||||
|  | 	defer fd.Close() | ||||||
|  | 
 | ||||||
|  | 	switch ext := filepath.Ext(filename); ext { | ||||||
|  | 	case ".json": | ||||||
|  | 		return json.NewDecoder(fd).Decode(data) | ||||||
|  | 	case ".yml", ".yaml": | ||||||
|  | 		return yaml.NewDecoder(fd).Decode(data) | ||||||
|  | 	default: | ||||||
|  | 		return errors.New("invalid format: " + ext) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func jsonYamlArg(cmd *cobra.Command, args []string) error { | ||||||
|  | 	if err := cobra.ExactArgs(1)(cmd, args); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch ext := filepath.Ext(args[0]); ext { | ||||||
|  | 	case ".json", ".yml", ".yaml": | ||||||
|  | 		return nil | ||||||
|  | 	default: | ||||||
|  | 		return errors.New("invalid format: " + ext) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} { | ||||||
|  | 	result := make(map[string]interface{}) | ||||||
|  | 	for k, v := range in { | ||||||
|  | 		result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v) | ||||||
|  | 	} | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cleanUpInterfaceArray(in []interface{}) []interface{} { | ||||||
|  | 	result := make([]interface{}, len(in)) | ||||||
|  | 	for i, v := range in { | ||||||
|  | 		result[i] = cleanUpMapValue(v) | ||||||
|  | 	} | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cleanUpMapValue(v interface{}) interface{} { | ||||||
|  | 	switch v := v.(type) { | ||||||
|  | 	case []interface{}: | ||||||
|  | 		return cleanUpInterfaceArray(v) | ||||||
|  | 	case map[interface{}]interface{}: | ||||||
|  | 		return cleanUpInterfaceMap(v) | ||||||
|  | 	default: | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ type User struct { | ||||||
| 	Perm         Permissions   `json:"perm"` | 	Perm         Permissions   `json:"perm"` | ||||||
| 	Commands     []string      `json:"commands"` | 	Commands     []string      `json:"commands"` | ||||||
| 	Sorting      files.Sorting `json:"sorting"` | 	Sorting      files.Sorting `json:"sorting"` | ||||||
| 	Fs           afero.Fs      `json:"-"` | 	Fs           afero.Fs      `json:"-" yaml:"-"` | ||||||
| 	Rules        []rules.Rule  `json:"rules"` | 	Rules        []rules.Rule  `json:"rules"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue