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 }