2022-08-28 13:21:57 +02:00
// Package rc provides the rc command.
2018-03-05 12:44:16 +01:00
package rc
import (
"bytes"
2019-06-17 10:34:30 +02:00
"context"
2018-03-05 12:44:16 +01:00
"encoding/json"
2021-11-04 11:12:57 +01:00
"errors"
2018-03-05 12:44:16 +01:00
"fmt"
2022-08-20 16:38:02 +02:00
"io"
2018-03-05 12:44:16 +01:00
"net/http"
"os"
"strings"
2019-07-28 19:47:38 +02:00
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs"
2019-10-11 17:55:04 +02:00
"github.com/rclone/rclone/fs/config/flags"
2019-07-28 19:47:38 +02:00
"github.com/rclone/rclone/fs/fshttp"
"github.com/rclone/rclone/fs/rc"
2020-12-12 16:35:30 +01:00
"github.com/rclone/rclone/fs/rc/jobs"
2018-03-05 12:44:16 +01:00
"github.com/spf13/cobra"
2018-04-26 17:59:10 +02:00
"github.com/spf13/pflag"
2018-03-05 12:44:16 +01:00
)
var (
2024-08-15 10:42:12 +02:00
noOutput = false
url = "http://localhost:5572/"
unixSocket = ""
jsonInput = ""
authUser = ""
authPass = ""
loopback = false
options [ ] string
arguments [ ] string
2018-03-05 12:44:16 +01:00
)
func init ( ) {
2019-10-11 17:58:11 +02:00
cmd . Root . AddCommand ( commandDefinition )
2019-10-11 17:55:04 +02:00
cmdFlags := commandDefinition . Flags ( )
2023-07-10 19:34:10 +02:00
flags . BoolVarP ( cmdFlags , & noOutput , "no-output" , "" , noOutput , "If set, don't output the JSON result" , "" )
flags . StringVarP ( cmdFlags , & url , "url" , "" , url , "URL to connect to rclone remote control" , "" )
2024-08-15 10:42:12 +02:00
flags . StringVarP ( cmdFlags , & unixSocket , "unix-socket" , "" , unixSocket , "Path to a unix domain socket to dial to, instead of opening a TCP connection directly" , "" )
2023-07-10 19:34:10 +02:00
flags . StringVarP ( cmdFlags , & jsonInput , "json" , "" , jsonInput , "Input JSON - use instead of key=value args" , "" )
flags . StringVarP ( cmdFlags , & authUser , "user" , "" , "" , "Username to use to rclone remote control" , "" )
flags . StringVarP ( cmdFlags , & authPass , "pass" , "" , "" , "Password to use to connect to rclone remote control" , "" )
flags . BoolVarP ( cmdFlags , & loopback , "loopback" , "" , false , "If set connect to this rclone instance not via HTTP" , "" )
flags . StringArrayVarP ( cmdFlags , & options , "opt" , "o" , options , "Option in the form name=value or name placed in the \"opt\" array" , "" )
flags . StringArrayVarP ( cmdFlags , & arguments , "arg" , "a" , arguments , "Argument placed in the \"arg\" array" , "" )
2018-03-05 12:44:16 +01:00
}
2019-10-11 17:58:11 +02:00
var commandDefinition = & cobra . Command {
2018-03-05 12:44:16 +01:00
Use : "rc commands parameter" ,
Short : ` Run a command against a running rclone. ` ,
2024-08-15 10:42:12 +02:00
Long : strings . ReplaceAll ( `
2018-11-03 13:40:50 +01:00
2024-08-15 10:42:12 +02:00
This runs a command against a running rclone . Use the | -- url | flag to
2018-11-03 13:40:50 +01:00
specify an non default URL to connect on . This can be either a
":port" which is taken to mean "http://localhost:port" or a
"host:port" which is taken to mean "http://host:port"
2024-08-15 10:42:12 +02:00
A username and password can be passed in with | -- user | and | -- pass | .
2018-11-03 13:40:50 +01:00
2024-08-15 10:42:12 +02:00
Note that | -- rc - addr | , | -- rc - user | , | -- rc - pass | will be read also for
| -- url | , | -- user | , | -- pass | .
The | -- unix - socket | flag can be used to connect over a unix socket like this
# start server on / tmp / my . socket
rclone rcd -- rc - addr unix : ///tmp/my.socket
# Connect to it
rclone rc -- unix - socket / tmp / my . socket core / stats
2018-03-05 12:44:16 +01:00
Arguments should be passed in as parameter = value .
The result will be returned as a JSON object by default .
2024-08-15 10:42:12 +02:00
The | -- json | parameter can be used to pass in a JSON blob as an input
2018-10-26 15:45:44 +02:00
instead of key = value arguments . This is the only way of passing in
more complicated values .
2024-08-15 10:42:12 +02:00
The | - o | / | -- opt | option can be used to set a key "opt" with key , value
options in the form | - o key = value | or | - o key | . It can be repeated as
2020-04-28 13:36:39 +02:00
many times as required . This is useful for rc commands which take the
"opt" parameter which by convention is a dictionary of strings .
- o key = value - o key2
Will place this in the "opt" value
{ "key" : "value" , "key2" , "" )
2024-08-15 10:42:12 +02:00
The | - a | / | -- arg | option can be used to set strings in the "arg" value . It
2020-04-28 13:36:39 +02:00
can be repeated as many times as required . This is useful for rc
commands which take the "arg" parameter which by convention is a list
of strings .
- a value - a value2
Will place this in the "arg" value
[ "value" , "value2" ]
2024-08-15 10:42:12 +02:00
Use | -- loopback | to connect to the rclone instance running | rclone rc | .
2019-06-08 09:45:55 +02:00
This is very useful for testing commands without having to run an
2020-10-13 23:49:58 +02:00
rclone rc server , e . g . :
2019-06-08 09:45:55 +02:00
rclone rc -- loopback operations / about fs = /
2024-08-15 10:42:12 +02:00
Use | rclone rc | to see a list of all possible commands . ` , "|", " ` " ) ,
2022-11-26 23:40:49 +01:00
Annotations : map [ string ] string {
"versionIntroduced" : "v1.40" ,
} ,
2018-03-05 12:44:16 +01:00
Run : func ( command * cobra . Command , args [ ] string ) {
2019-09-05 14:59:06 +02:00
cmd . CheckArgs ( 0 , 1e9 , command , args )
2018-03-05 12:44:16 +01:00
cmd . Run ( false , false , command , func ( ) error {
2019-09-04 21:21:10 +02:00
ctx := context . Background ( )
2018-11-03 13:40:50 +01:00
parseFlags ( )
2018-03-05 12:44:16 +01:00
if len ( args ) == 0 {
2019-09-04 21:21:10 +02:00
return list ( ctx )
2018-03-05 12:44:16 +01:00
}
2019-09-04 21:21:10 +02:00
return run ( ctx , args )
2018-03-05 12:44:16 +01:00
} )
} ,
}
2018-11-03 13:40:50 +01:00
// Parse the flags
func parseFlags ( ) {
// set alternates from alternate flags
setAlternateFlag ( "rc-addr" , & url )
setAlternateFlag ( "rc-user" , & authUser )
setAlternateFlag ( "rc-pass" , & authPass )
// If url is just :port then fix it up
if strings . HasPrefix ( url , ":" ) {
url = "localhost" + url
}
// if url is just host:port add http://
if ! strings . HasPrefix ( url , "http:" ) && ! strings . HasPrefix ( url , "https:" ) {
url = "http://" + url
}
// if url doesn't end with / add it
if ! strings . HasSuffix ( url , "/" ) {
url += "/"
}
}
2020-04-28 13:36:39 +02:00
// ParseOptions parses a slice of options in the form key=value or key
// into a map
func ParseOptions ( options [ ] string ) ( opt map [ string ] string ) {
opt = make ( map [ string ] string , len ( options ) )
for _ , option := range options {
equals := strings . IndexRune ( option , '=' )
key := option
value := ""
if equals >= 0 {
key = option [ : equals ]
value = option [ equals + 1 : ]
}
opt [ key ] = value
}
return opt
}
2018-11-03 13:40:50 +01:00
// If the user set flagName set the output to its value
func setAlternateFlag ( flagName string , output * string ) {
if rcFlag := pflag . Lookup ( flagName ) ; rcFlag != nil && rcFlag . Changed {
* output = rcFlag . Value . String ( )
2022-12-21 16:10:55 +01:00
if sliceValue , ok := rcFlag . Value . ( pflag . SliceValue ) ; ok {
stringSlice := sliceValue . GetSlice ( )
for _ , value := range stringSlice {
if value != "" {
* output = value
break
}
}
}
2018-11-03 13:40:50 +01:00
}
}
2023-09-20 11:22:17 +02:00
// Format an error and create a synthetic server return from it
func errorf ( status int , path string , format string , arg ... any ) ( out rc . Params , err error ) {
err = fmt . Errorf ( format , arg ... )
out = make ( rc . Params )
out [ "error" ] = err . Error ( )
out [ "path" ] = path
out [ "status" ] = status
return out , err
}
2018-03-05 12:44:16 +01:00
// do a call from (path, in) to (out, err).
//
// if err is set, out may be a valid error return or it may be nil
2019-09-04 21:21:10 +02:00
func doCall ( ctx context . Context , path string , in rc . Params ) ( out rc . Params , err error ) {
2019-06-08 09:45:55 +02:00
// If loopback set, short circuit HTTP request
if loopback {
call := rc . Calls . Get ( path )
if call == nil {
2023-09-20 11:22:17 +02:00
return errorf ( http . StatusBadRequest , path , "loopback: method %q not found" , path )
2019-06-08 09:45:55 +02:00
}
2020-12-12 16:35:30 +01:00
_ , out , err := jobs . NewJob ( ctx , call . Fn , in )
2019-08-13 12:51:16 +02:00
if err != nil {
2023-09-20 11:22:17 +02:00
return errorf ( http . StatusInternalServerError , path , "loopback: call failed: %w" , err )
2019-08-13 12:51:16 +02:00
}
// Reshape (serialize then deserialize) the data so it is in the form expected
err = rc . Reshape ( & out , out )
if err != nil {
2023-09-20 11:22:17 +02:00
return errorf ( http . StatusInternalServerError , path , "loopback: reshape failed: %w" , err )
2019-08-13 12:51:16 +02:00
}
return out , nil
2019-06-08 09:45:55 +02:00
}
2018-03-05 12:44:16 +01:00
// Do HTTP request
2024-08-15 10:42:12 +02:00
var client * http . Client
if unixSocket == "" {
client = fshttp . NewClient ( ctx )
} else {
client = fshttp . NewClientWithUnixSocket ( ctx , unixSocket )
}
2018-03-05 12:44:16 +01:00
url += path
data , err := json . Marshal ( in )
if err != nil {
2023-09-20 11:22:17 +02:00
return errorf ( http . StatusBadRequest , path , "failed to encode request: %w" , err )
2018-03-05 12:44:16 +01:00
}
2018-11-03 13:40:50 +01:00
2021-02-03 18:41:27 +01:00
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( data ) )
2018-11-03 13:40:50 +01:00
if err != nil {
2023-09-20 11:22:17 +02:00
return errorf ( http . StatusInternalServerError , path , "failed to make request: %w" , err )
2018-11-03 13:40:50 +01:00
}
req . Header . Set ( "Content-Type" , "application/json" )
if authUser != "" || authPass != "" {
req . SetBasicAuth ( authUser , authPass )
}
resp , err := client . Do ( req )
2018-03-05 12:44:16 +01:00
if err != nil {
2023-09-20 11:22:17 +02:00
return errorf ( http . StatusServiceUnavailable , path , "connection failed: %w" , err )
2018-03-05 12:44:16 +01:00
}
defer fs . CheckClose ( resp . Body , & err )
2023-09-20 11:22:17 +02:00
// Read response
var body [ ] byte
var bodyString string
body , err = io . ReadAll ( resp . Body )
bodyString = strings . TrimSpace ( string ( body ) )
if err != nil {
return errorf ( resp . StatusCode , "failed to read rc response: %s: %s" , resp . Status , bodyString )
2018-05-29 11:45:59 +02:00
}
2018-03-05 12:44:16 +01:00
// Parse output
out = make ( rc . Params )
2023-09-20 11:22:17 +02:00
err = json . NewDecoder ( strings . NewReader ( bodyString ) ) . Decode ( & out )
2018-03-05 12:44:16 +01:00
if err != nil {
2023-09-20 11:22:17 +02:00
return errorf ( resp . StatusCode , path , "failed to decode response: %w: %s" , err , bodyString )
2018-03-05 12:44:16 +01:00
}
// Check we got 200 OK
if resp . StatusCode != http . StatusOK {
2021-11-04 11:12:57 +01:00
err = fmt . Errorf ( "operation %q failed: %v" , path , out [ "error" ] )
2018-03-05 12:44:16 +01:00
}
return out , err
}
// Run the remote control command passed in
2019-09-04 21:21:10 +02:00
func run ( ctx context . Context , args [ ] string ) ( err error ) {
2018-03-05 12:44:16 +01:00
path := strings . Trim ( args [ 0 ] , "/" )
// parse input
in := make ( rc . Params )
2018-10-26 15:45:44 +02:00
params := args [ 1 : ]
if jsonInput == "" {
for _ , param := range params {
equals := strings . IndexRune ( param , '=' )
if equals < 0 {
2021-11-04 11:12:57 +01:00
return fmt . Errorf ( "no '=' found in parameter %q" , param )
2018-10-26 15:45:44 +02:00
}
key , value := param [ : equals ] , param [ equals + 1 : ]
in [ key ] = value
}
} else {
if len ( params ) > 0 {
return errors . New ( "can't use --json and parameters together" )
}
err = json . Unmarshal ( [ ] byte ( jsonInput ) , & in )
if err != nil {
2021-11-04 11:12:57 +01:00
return fmt . Errorf ( "bad --json input: %w" , err )
2018-03-05 12:44:16 +01:00
}
}
2020-04-28 13:36:39 +02:00
if len ( options ) > 0 {
in [ "opt" ] = ParseOptions ( options )
}
if len ( arguments ) > 0 {
in [ "arg" ] = arguments
}
2018-03-05 12:44:16 +01:00
// Do the call
2019-09-04 21:21:10 +02:00
out , callErr := doCall ( ctx , path , in )
2018-03-05 12:44:16 +01:00
// Write the JSON blob to stdout if required
if out != nil && ! noOutput {
2018-03-14 20:48:01 +01:00
err := rc . WriteJSON ( os . Stdout , out )
2018-03-05 12:44:16 +01:00
if err != nil {
2021-11-04 11:12:57 +01:00
return fmt . Errorf ( "failed to output JSON: %w" , err )
2018-03-05 12:44:16 +01:00
}
}
return callErr
}
// List the available commands to stdout
2019-09-04 21:21:10 +02:00
func list ( ctx context . Context ) error {
list , err := doCall ( ctx , "rc/list" , nil )
2018-03-05 12:44:16 +01:00
if err != nil {
2021-11-04 11:12:57 +01:00
return fmt . Errorf ( "failed to list: %w" , err )
2018-03-05 12:44:16 +01:00
}
commands , ok := list [ "commands" ] . ( [ ] interface { } )
if ! ok {
return errors . New ( "bad JSON" )
}
for _ , command := range commands {
info , ok := command . ( map [ string ] interface { } )
if ! ok {
return errors . New ( "bad JSON" )
}
2022-05-16 18:11:45 +02:00
fmt . Printf ( "### %s: %s {#%s}\n\n" , info [ "Path" ] , info [ "Title" ] , strings . ReplaceAll ( info [ "Path" ] . ( string ) , "/" , "-" ) )
2018-03-05 12:44:16 +01:00
fmt . Printf ( "%s\n\n" , info [ "Help" ] )
2018-11-03 13:40:50 +01:00
if authRequired := info [ "AuthRequired" ] ; authRequired != nil {
if authRequired . ( bool ) {
2020-05-17 12:44:34 +02:00
fmt . Printf ( "**Authentication is required for this call.**\n\n" )
2018-11-03 13:40:50 +01:00
}
}
2018-03-05 12:44:16 +01:00
}
return nil
}