diff --git a/fs/rc/internal.go b/fs/rc/internal.go index a813b006e..9c87dafb2 100644 --- a/fs/rc/internal.go +++ b/fs/rc/internal.go @@ -5,6 +5,7 @@ package rc import ( "context" "os" + "os/exec" "runtime" "time" @@ -335,3 +336,110 @@ func rcSetBlockProfileRate(ctx context.Context, in Params) (out Params, err erro runtime.SetBlockProfileRate(int(rate)) return nil, nil } + +func init() { + Add(Call{ + Path: "core/command", + AuthRequired: true, + Fn: rcRunCommand, + Title: "Run a rclone terminal command over rc.", + Help: `This takes the following parameters + +- command - a string with the command name +- arg - a list of arguments for the backend command +- opt - a map of string to string of options + +Returns + +- result - result from the backend command +- error - set if rclone exits with an error code + +For example + + rclone rc core/command command=ls -a mydrive:/ -o max-depth=1 + rclone rc core/command -a ls -a mydrive:/ -o max-depth=1 + +Returns + +` + "```" + ` +{ + "error": false, + "result": "" +} + +OR +{ + "error": true, + "result": "" +} + + +`, + }) +} + +// rcRunCommand runs an rclone command with the given args and flags +func rcRunCommand(ctx context.Context, in Params) (out Params, err error) { + + command, err := in.GetString("command") + if err != nil { + command = "" + } + + var opt = map[string]string{} + err = in.GetStructMissingOK("opt", &opt) + if err != nil { + return nil, err + } + + var arg = []string{} + err = in.GetStructMissingOK("arg", &arg) + if err != nil { + return nil, err + } + + var allArgs = []string{} + if command != "" { + // Add the command eg: ls to the args + allArgs = append(allArgs, command) + } + // Add all from arg + for _, cur := range arg { + allArgs = append(allArgs, cur) + } + + // Add flags to args for eg --max-depth 1 comes in as { max-depth 1 }. + // Convert it to [ max-depth, 1 ] and append to args list + for key, value := range opt { + if len(key) == 1 { + allArgs = append(allArgs, "-"+key) + } else { + allArgs = append(allArgs, "--"+key) + } + allArgs = append(allArgs, value) + } + + // Get the path for the current executable which was used to run rclone. + ex, err := os.Executable() + if err != nil { + return nil, err + } + + cmd := exec.CommandContext(ctx, ex, allArgs...) + + // Run the command and get the output for error and stdout combined. + output, err := cmd.CombinedOutput() + + if err != nil { + fs.Errorf(nil, "Command error %v", err) + return Params{ + "result": string(output), + "error": true, + }, nil + } + + return Params{ + "result": string(output), + "error": false, + }, nil +} diff --git a/fs/rc/internal_test.go b/fs/rc/internal_test.go index 118215301..64b212b1a 100644 --- a/fs/rc/internal_test.go +++ b/fs/rc/internal_test.go @@ -5,6 +5,8 @@ import ( "runtime" "testing" + "github.com/rclone/rclone/fstest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -119,3 +121,27 @@ func TestCoreQuit(t *testing.T) { _, err := call.Fn(context.Background(), in) require.Error(t, err) } + +// core/command: Runs a raw rclone command +func TestCoreCommand(t *testing.T) { + call := Calls.Get("core/command") + r := fstest.NewRun(t) + defer r.Finalise() + + in := Params{ + "command": "ls", + "opt": map[string]string{ + "max-depth": "1", + }, + "arg": []string{ + r.FremoteName, + }, + } + got, err := call.Fn(context.Background(), in) + require.NoError(t, err) + + errorBool, err := got.GetBool("error") + require.NoError(t, err) + + require.Equal(t, errorBool, false) +}