github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/cmd/rc/rc.go (about)

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