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.
 | ||||
| type JSONAuth struct { | ||||
| 	ReCaptcha *ReCaptcha | ||||
| 	ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"` | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| 
 | ||||
| 	if r.Body == nil { | ||||
|  |  | |||
|  | @ -14,6 +14,6 @@ const MethodNoAuth settings.AuthMethod = "noauth" | |||
| type NoAuth struct{} | ||||
| 
 | ||||
| // 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) | ||||
| } | ||||
|  |  | |||
|  | @ -14,11 +14,11 @@ const MethodProxyAuth settings.AuthMethod = "proxy" | |||
| 
 | ||||
| // ProxyAuth is a proxy implementation of an auther.
 | ||||
| type ProxyAuth struct { | ||||
| 	Header string | ||||
| 	Header string `json:"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) | ||||
| 	user, err := sto.Get(root, username) | ||||
| 	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 ( | ||||
| 	"crypto/rand" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/asdine/storm" | ||||
| 	"github.com/filebrowser/filebrowser/v2/storage" | ||||
|  | @ -11,6 +15,7 @@ import ( | |||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/pflag" | ||||
| 	v "github.com/spf13/viper" | ||||
| 	yaml "gopkg.in/yaml.v2" | ||||
| ) | ||||
| 
 | ||||
| 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) { | ||||
| 	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) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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"` | ||||
| 	Commands     []string      `json:"commands"` | ||||
| 	Sorting      files.Sorting `json:"sorting"` | ||||
| 	Fs           afero.Fs      `json:"-"` | ||||
| 	Fs           afero.Fs      `json:"-" yaml:"-"` | ||||
| 	Rules        []rules.Rule  `json:"rules"` | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue