github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/cmd/rc/rc.go (about)

     1  // Package rc provides the rc command.
     2  package rc
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/rclone/rclone/cmd"
    16  	"github.com/rclone/rclone/fs"
    17  	"github.com/rclone/rclone/fs/config/flags"
    18  	"github.com/rclone/rclone/fs/fshttp"
    19  	"github.com/rclone/rclone/fs/rc"
    20  	"github.com/rclone/rclone/fs/rc/jobs"
    21  	"github.com/spf13/cobra"
    22  	"github.com/spf13/pflag"
    23  )
    24  
    25  var (
    26  	noOutput  = false
    27  	url       = "http://localhost:5572/"
    28  	jsonInput = ""
    29  	authUser  = ""
    30  	authPass  = ""
    31  	loopback  = false
    32  	options   []string
    33  	arguments []string
    34  )
    35  
    36  func init() {
    37  	cmd.Root.AddCommand(commandDefinition)
    38  	cmdFlags := commandDefinition.Flags()
    39  	flags.BoolVarP(cmdFlags, &noOutput, "no-output", "", noOutput, "If set, don't output the JSON result", "")
    40  	flags.StringVarP(cmdFlags, &url, "url", "", url, "URL to connect to rclone remote control", "")
    41  	flags.StringVarP(cmdFlags, &jsonInput, "json", "", jsonInput, "Input JSON - use instead of key=value args", "")
    42  	flags.StringVarP(cmdFlags, &authUser, "user", "", "", "Username to use to rclone remote control", "")
    43  	flags.StringVarP(cmdFlags, &authPass, "pass", "", "", "Password to use to connect to rclone remote control", "")
    44  	flags.BoolVarP(cmdFlags, &loopback, "loopback", "", false, "If set connect to this rclone instance not via HTTP", "")
    45  	flags.StringArrayVarP(cmdFlags, &options, "opt", "o", options, "Option in the form name=value or name placed in the \"opt\" array", "")
    46  	flags.StringArrayVarP(cmdFlags, &arguments, "arg", "a", arguments, "Argument placed in the \"arg\" array", "")
    47  }
    48  
    49  var commandDefinition = &cobra.Command{
    50  	Use:   "rc commands parameter",
    51  	Short: `Run a command against a running rclone.`,
    52  	Long: `
    53  
    54  This runs a command against a running rclone.  Use the ` + "`--url`" + ` flag to
    55  specify an non default URL to connect on.  This can be either a
    56  ":port" which is taken to mean "http://localhost:port" or a
    57  "host:port" which is taken to mean "http://host:port"
    58  
    59  A username and password can be passed in with ` + "`--user`" + ` and ` + "`--pass`" + `.
    60  
    61  Note that ` + "`--rc-addr`, `--rc-user`, `--rc-pass`" + ` will be read also for
    62  ` + "`--url`, `--user`, `--pass`" + `.
    63  
    64  Arguments should be passed in as parameter=value.
    65  
    66  The result will be returned as a JSON object by default.
    67  
    68  The ` + "`--json`" + ` parameter can be used to pass in a JSON blob as an input
    69  instead of key=value arguments.  This is the only way of passing in
    70  more complicated values.
    71  
    72  The ` + "`-o`/`--opt`" + ` option can be used to set a key "opt" with key, value
    73  options in the form ` + "`-o key=value` or `-o key`" + `. It can be repeated as
    74  many times as required. This is useful for rc commands which take the
    75  "opt" parameter which by convention is a dictionary of strings.
    76  
    77      -o key=value -o key2
    78  
    79  Will place this in the "opt" value
    80  
    81      {"key":"value", "key2","")
    82  
    83  
    84  The ` + "`-a`/`--arg`" + ` option can be used to set strings in the "arg" value. It
    85  can be repeated as many times as required. This is useful for rc
    86  commands which take the "arg" parameter which by convention is a list
    87  of strings.
    88  
    89      -a value -a value2
    90  
    91  Will place this in the "arg" value
    92  
    93      ["value", "value2"]
    94  
    95  Use ` + "`--loopback`" + ` to connect to the rclone instance running ` + "`rclone rc`" + `.
    96  This is very useful for testing commands without having to run an
    97  rclone rc server, e.g.:
    98  
    99      rclone rc --loopback operations/about fs=/
   100  
   101  Use ` + "`rclone rc`" + ` to see a list of all possible commands.`,
   102  	Annotations: map[string]string{
   103  		"versionIntroduced": "v1.40",
   104  	},
   105  	Run: func(command *cobra.Command, args []string) {
   106  		cmd.CheckArgs(0, 1e9, command, args)
   107  		cmd.Run(false, false, command, func() error {
   108  			ctx := context.Background()
   109  			parseFlags()
   110  			if len(args) == 0 {
   111  				return list(ctx)
   112  			}
   113  			return run(ctx, args)
   114  		})
   115  	},
   116  }
   117  
   118  // Parse the flags
   119  func parseFlags() {
   120  	// set alternates from alternate flags
   121  	setAlternateFlag("rc-addr", &url)
   122  	setAlternateFlag("rc-user", &authUser)
   123  	setAlternateFlag("rc-pass", &authPass)
   124  	// If url is just :port then fix it up
   125  	if strings.HasPrefix(url, ":") {
   126  		url = "localhost" + url
   127  	}
   128  	// if url is just host:port add http://
   129  	if !strings.HasPrefix(url, "http:") && !strings.HasPrefix(url, "https:") {
   130  		url = "http://" + url
   131  	}
   132  	// if url doesn't end with / add it
   133  	if !strings.HasSuffix(url, "/") {
   134  		url += "/"
   135  	}
   136  }
   137  
   138  // ParseOptions parses a slice of options in the form key=value or key
   139  // into a map
   140  func ParseOptions(options []string) (opt map[string]string) {
   141  	opt = make(map[string]string, len(options))
   142  	for _, option := range options {
   143  		equals := strings.IndexRune(option, '=')
   144  		key := option
   145  		value := ""
   146  		if equals >= 0 {
   147  			key = option[:equals]
   148  			value = option[equals+1:]
   149  		}
   150  		opt[key] = value
   151  	}
   152  	return opt
   153  }
   154  
   155  // If the user set flagName set the output to its value
   156  func setAlternateFlag(flagName string, output *string) {
   157  	if rcFlag := pflag.Lookup(flagName); rcFlag != nil && rcFlag.Changed {
   158  		*output = rcFlag.Value.String()
   159  		if sliceValue, ok := rcFlag.Value.(pflag.SliceValue); ok {
   160  			stringSlice := sliceValue.GetSlice()
   161  			for _, value := range stringSlice {
   162  				if value != "" {
   163  					*output = value
   164  					break
   165  				}
   166  			}
   167  		}
   168  	}
   169  }
   170  
   171  // Format an error and create a synthetic server return from it
   172  func errorf(status int, path string, format string, arg ...any) (out rc.Params, err error) {
   173  	err = fmt.Errorf(format, arg...)
   174  	out = make(rc.Params)
   175  	out["error"] = err.Error()
   176  	out["path"] = path
   177  	out["status"] = status
   178  	return out, err
   179  }
   180  
   181  // do a call from (path, in) to (out, err).
   182  //
   183  // if err is set, out may be a valid error return or it may be nil
   184  func doCall(ctx context.Context, path string, in rc.Params) (out rc.Params, err error) {
   185  	// If loopback set, short circuit HTTP request
   186  	if loopback {
   187  		call := rc.Calls.Get(path)
   188  		if call == nil {
   189  			return errorf(http.StatusBadRequest, path, "loopback: method %q not found", path)
   190  		}
   191  		_, out, err := jobs.NewJob(ctx, call.Fn, in)
   192  		if err != nil {
   193  			return errorf(http.StatusInternalServerError, path, "loopback: call failed: %w", err)
   194  		}
   195  		// Reshape (serialize then deserialize) the data so it is in the form expected
   196  		err = rc.Reshape(&out, out)
   197  		if err != nil {
   198  			return errorf(http.StatusInternalServerError, path, "loopback: reshape failed: %w", err)
   199  		}
   200  		return out, nil
   201  	}
   202  
   203  	// Do HTTP request
   204  	client := fshttp.NewClient(ctx)
   205  	url += path
   206  	data, err := json.Marshal(in)
   207  	if err != nil {
   208  		return errorf(http.StatusBadRequest, path, "failed to encode request: %w", err)
   209  	}
   210  
   211  	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data))
   212  	if err != nil {
   213  		return errorf(http.StatusInternalServerError, path, "failed to make request: %w", err)
   214  	}
   215  
   216  	req.Header.Set("Content-Type", "application/json")
   217  	if authUser != "" || authPass != "" {
   218  		req.SetBasicAuth(authUser, authPass)
   219  	}
   220  
   221  	resp, err := client.Do(req)
   222  	if err != nil {
   223  		return errorf(http.StatusServiceUnavailable, path, "connection failed: %w", err)
   224  	}
   225  	defer fs.CheckClose(resp.Body, &err)
   226  
   227  	// Read response
   228  	var body []byte
   229  	var bodyString string
   230  	body, err = io.ReadAll(resp.Body)
   231  	bodyString = strings.TrimSpace(string(body))
   232  	if err != nil {
   233  		return errorf(resp.StatusCode, "failed to read rc response: %s: %s", resp.Status, bodyString)
   234  	}
   235  
   236  	// Parse output
   237  	out = make(rc.Params)
   238  	err = json.NewDecoder(strings.NewReader(bodyString)).Decode(&out)
   239  	if err != nil {
   240  		return errorf(resp.StatusCode, path, "failed to decode response: %w: %s", err, bodyString)
   241  	}
   242  
   243  	// Check we got 200 OK
   244  	if resp.StatusCode != http.StatusOK {
   245  		err = fmt.Errorf("operation %q failed: %v", path, out["error"])
   246  	}
   247  
   248  	return out, err
   249  }
   250  
   251  // Run the remote control command passed in
   252  func run(ctx context.Context, args []string) (err error) {
   253  	path := strings.Trim(args[0], "/")
   254  
   255  	// parse input
   256  	in := make(rc.Params)
   257  	params := args[1:]
   258  	if jsonInput == "" {
   259  		for _, param := range params {
   260  			equals := strings.IndexRune(param, '=')
   261  			if equals < 0 {
   262  				return fmt.Errorf("no '=' found in parameter %q", param)
   263  			}
   264  			key, value := param[:equals], param[equals+1:]
   265  			in[key] = value
   266  		}
   267  	} else {
   268  		if len(params) > 0 {
   269  			return errors.New("can't use --json and parameters together")
   270  		}
   271  		err = json.Unmarshal([]byte(jsonInput), &in)
   272  		if err != nil {
   273  			return fmt.Errorf("bad --json input: %w", err)
   274  		}
   275  	}
   276  	if len(options) > 0 {
   277  		in["opt"] = ParseOptions(options)
   278  	}
   279  	if len(arguments) > 0 {
   280  		in["arg"] = arguments
   281  	}
   282  
   283  	// Do the call
   284  	out, callErr := doCall(ctx, path, in)
   285  
   286  	// Write the JSON blob to stdout if required
   287  	if out != nil && !noOutput {
   288  		err := rc.WriteJSON(os.Stdout, out)
   289  		if err != nil {
   290  			return fmt.Errorf("failed to output JSON: %w", err)
   291  		}
   292  	}
   293  
   294  	return callErr
   295  }
   296  
   297  // List the available commands to stdout
   298  func list(ctx context.Context) error {
   299  	list, err := doCall(ctx, "rc/list", nil)
   300  	if err != nil {
   301  		return fmt.Errorf("failed to list: %w", err)
   302  	}
   303  	commands, ok := list["commands"].([]interface{})
   304  	if !ok {
   305  		return errors.New("bad JSON")
   306  	}
   307  	for _, command := range commands {
   308  		info, ok := command.(map[string]interface{})
   309  		if !ok {
   310  			return errors.New("bad JSON")
   311  		}
   312  		fmt.Printf("### %s: %s {#%s}\n\n", info["Path"], info["Title"], strings.ReplaceAll(info["Path"].(string), "/", "-"))
   313  		fmt.Printf("%s\n\n", info["Help"])
   314  		if authRequired := info["AuthRequired"]; authRequired != nil {
   315  			if authRequired.(bool) {
   316  				fmt.Printf("**Authentication is required for this call.**\n\n")
   317  			}
   318  		}
   319  	}
   320  	return nil
   321  }