From 88effa0889fa372648056c1b9a21afe5fb6f082e Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 2 Jan 2019 19:06:35 +0000 Subject: [PATCH] feat: add shell opt License: MIT Signed-off-by: Henrique Dias --- bolt/config.go | 11 ---- cmd/cmds_add.go | 8 +-- cmd/cmds_ls.go | 8 +-- cmd/cmds_rm.go | 8 +-- cmd/config.go | 2 + cmd/config_init.go | 6 +- cmd/root.go | 4 +- frontend | 2 +- http/http.go | 1 - http/resource.go | 6 +- http/settings.go | 14 ++-- http/websockets.go | 19 ++---- types/runner.go | 143 +++++++++++++++++++++------------------- types/settings.go | 16 +++-- types/storage_config.go | 28 +++----- 15 files changed, 127 insertions(+), 149 deletions(-) diff --git a/bolt/config.go b/bolt/config.go index 9941975d..f1b82599 100644 --- a/bolt/config.go +++ b/bolt/config.go @@ -38,17 +38,6 @@ func (c ConfigStore) SaveSettings(s *types.Settings) error { return c.Save("settings", s) } -// GetRunner is an helper method to get a runner object. -func (c ConfigStore) GetRunner() (*types.Runner, error) { - runner := &types.Runner{} - return runner, c.Get("runner", runner) -} - -// SaveRunner is an helper method to set the runner object -func (c ConfigStore) SaveRunner(r *types.Runner) error { - return c.Save("runner", r) -} - // GetAuther is an helper method to get an auther object. func (c ConfigStore) GetAuther(t types.AuthMethod) (types.Auther, error) { if t == auth.MethodJSONAuth { diff --git a/cmd/cmds_add.go b/cmd/cmds_add.go index 3957180f..06d78618 100644 --- a/cmd/cmds_add.go +++ b/cmd/cmds_add.go @@ -21,15 +21,15 @@ var cmdsAddCmd = &cobra.Command{ db := getDB() defer db.Close() st := getStore(db) - r, err := st.Config.GetRunner() + s, err := st.Config.GetSettings() checkErr(err) evt := mustGetString(cmd, "event") command := mustGetString(cmd, "command") - r.Commands[evt] = append(r.Commands[evt], command) - err = st.Config.SaveRunner(r) + s.Commands[evt] = append(s.Commands[evt], command) + err = st.Config.SaveSettings(s) checkErr(err) - printEvents(r.Commands) + printEvents(s.Commands) }, } diff --git a/cmd/cmds_ls.go b/cmd/cmds_ls.go index 30f07580..b8368828 100644 --- a/cmd/cmds_ls.go +++ b/cmd/cmds_ls.go @@ -18,16 +18,16 @@ var cmdsLsCmd = &cobra.Command{ db := getDB() defer db.Close() st := getStore(db) - r, err := st.Config.GetRunner() + s, err := st.Config.GetSettings() checkErr(err) evt := mustGetString(cmd, "event") if evt == "" { - printEvents(r.Commands) + printEvents(s.Commands) } else { show := map[string][]string{} - show["before_"+evt] = r.Commands["before_"+evt] - show["after_"+evt] = r.Commands["after_"+evt] + show["before_"+evt] = s.Commands["before_"+evt] + show["after_"+evt] = s.Commands["after_"+evt] printEvents(show) } }, diff --git a/cmd/cmds_rm.go b/cmd/cmds_rm.go index c4226591..a05dab97 100644 --- a/cmd/cmds_rm.go +++ b/cmd/cmds_rm.go @@ -21,16 +21,16 @@ var cmdsRmCmd = &cobra.Command{ db := getDB() defer db.Close() st := getStore(db) - r, err := st.Config.GetRunner() + s, err := st.Config.GetSettings() checkErr(err) evt := mustGetString(cmd, "event") i, err := cmd.Flags().GetUint("index") checkErr(err) - r.Commands[evt] = append(r.Commands[evt][:i], r.Commands[evt][i+1:]...) - err = st.Config.SaveRunner(r) + s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][i+1:]...) + err = st.Config.SaveSettings(s) checkErr(err) - printEvents(r.Commands) + printEvents(s.Commands) }, } diff --git a/cmd/config.go b/cmd/config.go index 3cc46ea0..1ea7d518 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -13,6 +13,8 @@ import ( "github.com/spf13/cobra" ) + + func init() { rootCmd.AddCommand(configCmd) } diff --git a/cmd/config_init.go b/cmd/config_init.go index 4d8751f5..c580cf05 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -53,7 +53,7 @@ override the options.`, checkErr(err) defer db.Close() - saveConfig(db, settings, &types.Runner{}, auther) + saveConfig(db, settings, auther) fmt.Printf(` Congratulations! You've set up your database to use with File Browser. @@ -64,12 +64,10 @@ need to call the main command to boot up the server. }, } -func saveConfig(db *storm.DB, s *types.Settings, r *types.Runner, a types.Auther) { +func saveConfig(db *storm.DB, s *types.Settings, a types.Auther) { st := getStore(db) err := st.Config.SaveSettings(s) checkErr(err) - err = st.Config.SaveRunner(r) - checkErr(err) err = st.Config.SaveAuther(a) checkErr(err) } diff --git a/cmd/root.go b/cmd/root.go index cdbfd53b..af361ece 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,8 +65,6 @@ listening on loalhost on a random port. Use the flags to change it.`, checkErr(err) env.Auther, err = env.Store.Config.GetAuther(env.Settings.AuthMethod) checkErr(err) - env.Runner, err = env.Store.Config.GetRunner() - checkErr(err) startServer(cmd, env) }, @@ -133,7 +131,7 @@ func quickSetup(cmd *cobra.Command) { checkErr(err) defer db.Close() - saveConfig(db, settings, &types.Runner{}, &auth.JSONAuth{}) + saveConfig(db, settings, &auth.JSONAuth{}) st := getStore(db) err = st.Users.Save(user) diff --git a/frontend b/frontend index eb33671e..76ee2aa4 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit eb33671e572daddf57a3bbdf47e3f09865e27837 +Subproject commit 76ee2aa4d628d03e5d444a98a41b7d59bdba9b7e diff --git a/http/http.go b/http/http.go index dfc775d6..7fc013a0 100644 --- a/http/http.go +++ b/http/http.go @@ -27,7 +27,6 @@ type modifyRequest struct { // Env contains the required info for FB to run. type Env struct { Auther types.Auther - Runner *types.Runner Settings *types.Settings Store *types.Store mux sync.Mutex // settings mutex for Auther, Runner and Settings changes. diff --git a/http/resource.go b/http/resource.go index 02684e27..73eadec7 100644 --- a/http/resource.go +++ b/http/resource.go @@ -105,7 +105,7 @@ func (e *Env) resourceDeleteHandler(w http.ResponseWriter, r *http.Request) { return } - err := e.Runner.Run(func() error { + err := e.Settings.Run(func() error { return user.Fs.RemoveAll(path) }, "delete", path, "", user) @@ -156,7 +156,7 @@ func (e *Env) resourcePostPutHandler(w http.ResponseWriter, r *http.Request) { } } - err := e.Runner.Run(func() error { + err := e.Settings.Run(func() error { file, err := user.Fs.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) if err != nil { return err @@ -222,7 +222,7 @@ func (e *Env) resourcePatchHandler(w http.ResponseWriter, r *http.Request) { } } - err = e.Runner.Run(func() error { + err = e.Settings.Run(func() error { if action == "copy" { return fileutils.Copy(user.Fs, src, dst) } diff --git a/http/settings.go b/http/settings.go index b6750aeb..f0ab4c51 100644 --- a/http/settings.go +++ b/http/settings.go @@ -13,6 +13,7 @@ type settingsData struct { Defaults types.UserDefaults `json:"defaults"` Rules []types.Rule `json:"rules"` Branding types.Branding `json:"branding"` + Shell []string `json:"shell"` Commands map[string][]string `json:"commands"` } @@ -27,7 +28,8 @@ func (e *Env) settingsGetHandler(w http.ResponseWriter, r *http.Request) { Defaults: e.Settings.Defaults, Rules: e.Settings.Rules, Branding: e.Settings.Branding, - Commands: e.Runner.Commands, + Shell: e.Settings.Shell, + Commands: e.Settings.Commands, } renderJSON(w, r, data) @@ -49,13 +51,6 @@ func (e *Env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { e.mux.Lock() defer e.mux.Unlock() - runner := &types.Runner{Commands: req.Commands} - err = e.Store.Config.SaveRunner(runner) - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - settings := &types.Settings{} err = copier.Copy(settings, e.Settings) if err != nil { @@ -67,6 +62,8 @@ func (e *Env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { settings.Defaults = req.Defaults settings.Rules = req.Rules settings.Branding = req.Branding + settings.Shell = req.Shell + settings.Commands = req.Commands err = e.Store.Config.SaveSettings(settings) if err != nil { @@ -74,6 +71,5 @@ func (e *Env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { return } - e.Runner = runner e.Settings = settings } diff --git a/http/websockets.go b/http/websockets.go index 7bce98f2..24f0134a 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -38,7 +38,7 @@ func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { } defer conn.Close() - var command []string + var raw string for { _, msg, err := conn.ReadMessage() @@ -47,13 +47,13 @@ func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { return } - command = strings.Split(string(msg), " ") - if len(command) != 0 { + raw = strings.TrimSpace(string(msg)) + if raw != "" { break } } - if !user.CanExecute(command[0]) { + if !user.CanExecute(strings.Split(raw, " ")[0]) { err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed) if err != nil { wsErr(conn, r, http.StatusInternalServerError, err) @@ -62,15 +62,7 @@ func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { return } - if _, err := exec.LookPath(command[0]); err != nil { - err := conn.WriteMessage(websocket.TextMessage, cmdNotImplemented) - if err != nil { - wsErr(conn, r, http.StatusInternalServerError, err) - } - - return - } - + command, err := e.Settings.ParseCommand(raw) path := strings.TrimPrefix(r.URL.Path, "/api/command") dir := afero.FullBaseFsPath(user.Fs.(*afero.BasePathFs), path) cmd := exec.Command(command[0], command[1:]...) @@ -81,6 +73,7 @@ func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { wsErr(conn, r, http.StatusInternalServerError, err) return } + stderr, err := cmd.StderrPipe() if err != nil { wsErr(conn, r, http.StatusInternalServerError, err) diff --git a/types/runner.go b/types/runner.go index 6cc4fb34..84fabcab 100644 --- a/types/runner.go +++ b/types/runner.go @@ -19,81 +19,90 @@ var defaultEvents = []string{ "delete", } -// Runner runs certain commands. -type Runner struct { - Commands map[string][]string `json:"commands"` -} - // Run runs the hooks for the before and after event. -func (r Runner) Run(fn func() error, event string, path string, dst string, u *User) error { - path = afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path) - dst = afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), dst) - err := r.do("before_"+event, path, dst, u) - if err != nil { - return err - } +func (s *Settings) Run(fn func() error, evt, path, dst string, user *User) error { + path = afero.FullBaseFsPath(user.Fs.(*afero.BasePathFs), path) + dst = afero.FullBaseFsPath(user.Fs.(*afero.BasePathFs), dst) - err = fn() - if err != nil { - return err - } - - return r.do("after_"+event, path, dst, u) -} - -func (r Runner) do(event string, path string, destination string, user *User) error { - commands := []string{} - - if val, ok := r.Commands[event]; ok { - commands = append(commands, val...) - } - - for _, command := range commands { - if command == "" { - continue - } - - args := strings.Split(command, " ") - nonblock := false - - if len(args) > 1 && args[len(args)-1] == "&" { - nonblock = true - args = args[:len(args)-1] - } - - command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " ")) - if err != nil { - return err - } - - cmd := exec.Command(command, args...) - cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) - cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) - cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", event)) - cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) - - if destination != "" { - cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", destination)) - } - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if nonblock { - log.Printf("[INFO] Nonblocking Command:\"%s %s\"", command, strings.Join(args, " ")) - if err := cmd.Start(); err != nil { + if val, ok := s.Commands["before_"+evt]; ok { + for _, command := range val { + err := s.exec(command, "before_"+evt, path, dst, user) + if err != nil { return err } - - continue } + } - log.Printf("[INFO] Blocking Command:\"%s %s\"", command, strings.Join(args, " ")) - if err := cmd.Run(); err != nil { - return err + err := fn() + if err != nil { + return err + } + + if val, ok := s.Commands["after_"+evt]; ok { + for _, command := range val { + err := s.exec(command, "after_"+evt, path, dst, user) + if err != nil { + return err + } } } return nil } + +// ParseCommand parses the command taking in account +func (s *Settings) ParseCommand(raw string) ([]string, error) { + command := []string{} + + if len(s.Shell) == 0 { + cmd, args, err := caddy.SplitCommandAndArgs(raw) + if err != nil { + return nil, err + } + + _, err = exec.LookPath(cmd) + if err != nil { + return nil, err + } + + command = append(command, cmd) + command = append(command, args...) + } else { + command = append(s.Shell, raw) + } + + return command, nil +} + +func (s *Settings) exec(raw, evt, path, dst string, user *User) error { + blocking := true + + if strings.HasSuffix(raw, "&") { + blocking = false + raw = strings.TrimSpace(strings.TrimSuffix(raw, "&")) + } + + command, err := s.ParseCommand(raw) + if err != nil { + return err + } + + cmd := exec.Command(command[0], command[1:]...) + cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) + cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) + cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt)) + cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) + cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst)) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if !blocking { + log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " ")) + return cmd.Start() + } + + log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " ")) + return cmd.Run() +} diff --git a/types/settings.go b/types/settings.go index 9410ebc4..c4042245 100644 --- a/types/settings.go +++ b/types/settings.go @@ -5,13 +5,15 @@ type AuthMethod string // Settings contain the main settings of the application. type Settings struct { - Key []byte `json:"key"` - BaseURL string `json:"baseURL"` - Signup bool `json:"signup"` - Defaults UserDefaults `json:"defaults"` - AuthMethod AuthMethod `json:"authMethod"` - Branding Branding `json:"branding"` - Rules []Rule `json:"rules"` // TODO: use this add to cli + Key []byte `json:"key"` + BaseURL string `json:"baseURL"` + Signup bool `json:"signup"` + Defaults UserDefaults `json:"defaults"` + AuthMethod AuthMethod `json:"authMethod"` + Branding Branding `json:"branding"` + Commands map[string][]string `json:"commands"` + Shell []string `json:"shell"` // TODO add to cli + Rules []Rule `json:"rules"` // TODO: use this add to cli } // IsAllowed matches the rules against the url. diff --git a/types/storage_config.go b/types/storage_config.go index a33e4f95..77b17540 100644 --- a/types/storage_config.go +++ b/types/storage_config.go @@ -6,8 +6,6 @@ import "strings" type ConfigStore interface { GetSettings() (*Settings, error) SaveSettings(*Settings) error - SaveRunner(*Runner) error - GetRunner() (*Runner, error) GetAuther(AuthMethod) (Auther, error) SaveAuther(Auther) error } @@ -46,31 +44,25 @@ func (v ConfigVerify) SaveSettings(s *Settings) error { s.Rules = []Rule{} } - return v.Store.SaveSettings(s) -} + if s.Shell == nil { + s.Shell = []string{} + } -// GetRunner wraps a ConfigStore.GetRunner -func (v ConfigVerify) GetRunner() (*Runner, error) { - return v.Store.GetRunner() -} - -// SaveRunner wraps a ConfigStore.SaveRunner -func (v ConfigVerify) SaveRunner(r *Runner) error { - if r.Commands == nil { - r.Commands = map[string][]string{} + if s.Commands == nil { + s.Commands = map[string][]string{} } for _, event := range defaultEvents { - if _, ok := r.Commands["before_"+event]; !ok { - r.Commands["before_"+event] = []string{} + if _, ok := s.Commands["before_"+event]; !ok { + s.Commands["before_"+event] = []string{} } - if _, ok := r.Commands["after_"+event]; !ok { - r.Commands["after_"+event] = []string{} + if _, ok := s.Commands["after_"+event]; !ok { + s.Commands["after_"+event] = []string{} } } - return v.Store.SaveRunner(r) + return v.Store.SaveSettings(s) } // GetAuther wraps a ConfigStore.GetAuther