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 }