From d80fdad6da85e85ce6497c938131d42c1b5cccbc Mon Sep 17 00:00:00 2001
From: Nick Craig-Wood <nick@craig-wood.com>
Date: Tue, 28 Apr 2020 13:02:19 +0100
Subject: [PATCH] rc: implement backend/command for running backend commands
 remotely

---
 fs/operations/rc.go      | 85 ++++++++++++++++++++++++++++++++++++++++
 fs/operations/rc_test.go | 41 +++++++++++++++++++
 2 files changed, 126 insertions(+)

diff --git a/fs/operations/rc.go b/fs/operations/rc.go
index 3c29c3c61..346a557e0 100644
--- a/fs/operations/rc.go
+++ b/fs/operations/rc.go
@@ -373,3 +373,88 @@ func rcFsInfo(ctx context.Context, in rc.Params) (out rc.Params, err error) {
 	}
 	return out, nil
 }
+
+func init() {
+	rc.Add(rc.Call{
+		Path:         "backend/command",
+		AuthRequired: true,
+		Fn:           rcBackend,
+		Title:        "Runs a backend command.",
+		Help: `This takes the following parameters
+
+- command - a string with the command name
+- fs - a remote name string eg "drive:"
+- 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
+
+For example
+
+    rclone rc backend/command command=noop fs=. -o echo=yes -o blue -a path1 -a path2
+
+Returns
+
+` + "```" + `
+{
+	"result": {
+		"arg": [
+			"path1",
+			"path2"
+		],
+		"name": "noop",
+		"opt": {
+			"blue": "",
+			"echo": "yes"
+		}
+	}
+}
+` + "```" + `
+
+Note that this is the direct equivalent of using this "backend"
+command:
+
+    rclone backend noop . -o echo=yes -o blue path1 path2
+
+Note that arguments must be preceeded by the "-a" flag
+
+See the [backend](/commands/rclone_backend/) command for more information.
+`,
+	})
+}
+
+// Make a public link
+func rcBackend(ctx context.Context, in rc.Params) (out rc.Params, err error) {
+	f, err := rc.GetFs(in)
+	if err != nil {
+		return nil, err
+	}
+	doCommand := f.Features().Command
+	if doCommand == nil {
+		return nil, errors.Errorf("%v: doesn't support backend commands", f)
+	}
+	command, err := in.GetString("command")
+	if err != nil {
+		return nil, err
+	}
+	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
+	}
+	result, err := doCommand(context.Background(), command, arg, opt)
+	if err != nil {
+		return nil, errors.Wrapf(err, "command %q failed", command)
+
+	}
+	out = make(rc.Params)
+	out["result"] = result
+	return out, nil
+}
diff --git a/fs/operations/rc_test.go b/fs/operations/rc_test.go
index c888509a5..36117c284 100644
--- a/fs/operations/rc_test.go
+++ b/fs/operations/rc_test.go
@@ -434,3 +434,44 @@ func TestRcFsInfo(t *testing.T) {
 	assert.Equal(t, features, got["Features"])
 
 }
+
+// operations/command: Runs a backend command
+func TestRcCommand(t *testing.T) {
+	r, call := rcNewRun(t, "backend/command")
+	defer r.Finalise()
+	in := rc.Params{
+		"fs":      r.FremoteName,
+		"command": "noop",
+		"opt": map[string]string{
+			"echo": "true",
+			"blue": "",
+		},
+		"arg": []string{
+			"path1",
+			"path2",
+		},
+	}
+	got, err := call.Fn(context.Background(), in)
+	if err != nil {
+		assert.False(t, r.Fremote.Features().IsLocal, "mustn't fail on local remote")
+		assert.Contains(t, err.Error(), "command not found")
+		return
+	}
+	want := rc.Params{"result": map[string]interface{}{
+		"arg": []string{
+			"path1",
+			"path2",
+		},
+		"name": "noop",
+		"opt": map[string]string{
+			"blue": "",
+			"echo": "true",
+		},
+	}}
+	assert.Equal(t, want, got)
+	errTxt := "explosion in the sausage factory"
+	in["opt"].(map[string]string)["error"] = errTxt
+	_, err = call.Fn(context.Background(), in)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), errTxt)
+}