diff --git a/fs/rc/rc.go b/fs/rc/rc.go index 0cb68aa2a..91be88584 100644 --- a/fs/rc/rc.go +++ b/fs/rc/rc.go @@ -10,6 +10,7 @@ package rc import ( "encoding/json" "io" + "mime" "net/http" _ "net/http/pprof" // install the pprof http handlers "strings" @@ -17,12 +18,14 @@ import ( "github.com/ncw/rclone/cmd/serve/httplib" "github.com/ncw/rclone/fs" "github.com/pkg/errors" + "github.com/skratchdot/open-golang/open" ) // Options contains options for the remote control server type Options struct { HTTPOptions httplib.Options - Enabled bool + Enabled bool // set to enable the server + Files string // set to enable serving files } // DefaultOpt is the default values used for Options @@ -45,7 +48,8 @@ func Start(opt *Options) { // server contains everything to run the server type server struct { - srv *httplib.Server + srv *httplib.Server + files http.Handler } func newServer(opt *Options) *server { @@ -55,6 +59,17 @@ func newServer(opt *Options) *server { srv: httplib.NewServer(mux, &opt.HTTPOptions), } mux.HandleFunc("/", s.handler) + + // Add some more mime types which are often missing + mime.AddExtensionType(".wasm", "application/wasm") + mime.AddExtensionType(".js", "application/javascript") + + // File handling + s.files = http.NewServeMux() + if opt.Files != "" { + fs.Logf(nil, "Serving files from %q", opt.Files) + s.files = http.FileServer(http.Dir(opt.Files)) + } return s } @@ -65,6 +80,10 @@ func (s *server) serve() { fs.Errorf(nil, "Opening listener: %v", err) } fs.Logf(nil, "Serving remote control on %s", s.srv.URL()) + // Open the files in the browser if set + if s.files != nil { + _ = open.Start(s.srv.URL()) + } s.srv.Wait() } @@ -75,32 +94,58 @@ func WriteJSON(w io.Writer, out Params) error { return enc.Encode(out) } +// writeError writes a formatted error to the output +func writeError(path string, in Params, w http.ResponseWriter, err error, status int) { + fs.Errorf(nil, "rc: %q: error: %v", path, err) + // Adjust the error return for some well known errors + switch errors.Cause(err) { + case fs.ErrorDirNotFound, fs.ErrorObjectNotFound: + status = http.StatusNotFound + } + w.WriteHeader(status) + err = WriteJSON(w, Params{ + "error": err.Error(), + "input": in, + }) + if err != nil { + // can't return the error at this point + fs.Errorf(nil, "rc: failed to write JSON output: %v", err) + } +} + // handler reads incoming requests and dispatches them func (s *server) handler(w http.ResponseWriter, r *http.Request) { path := strings.Trim(r.URL.Path, "/") - in := make(Params) - writeError := func(err error, status int) { - fs.Errorf(nil, "rc: %q: error: %v", path, err) - w.WriteHeader(status) - err = WriteJSON(w, Params{ - "error": err.Error(), - "input": in, - }) - if err != nil { - // can't return the error at this point - fs.Errorf(nil, "rc: failed to write JSON output: %v", err) - } + w.Header().Add("Access-Control-Allow-Origin", "*") + + // echo back access control headers client needs + reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers") + w.Header().Add("Access-Control-Allow-Headers", reqAccessHeaders) + + switch r.Method { + case "POST": + s.handlePost(w, r, path) + case "OPTIONS": + s.handleOptions(w, r, path) + case "GET": + s.handleGet(w, r, path) + default: + writeError(path, nil, w, errors.Errorf("method %q not allowed", r.Method), http.StatusMethodNotAllowed) + return } +} +func (s *server) handlePost(w http.ResponseWriter, r *http.Request, path string) { // Parse the POST and URL parameters into r.Form, for others r.Form will be empty value err := r.ParseForm() if err != nil { - writeError(errors.Wrap(err, "failed to parse form/URL parameters"), http.StatusBadRequest) + writeError(path, nil, w, errors.Wrap(err, "failed to parse form/URL parameters"), http.StatusBadRequest) return } // Read the POST and URL parameters into in + in := make(Params) for k, vs := range r.Form { if len(vs) > 0 { in[k] = vs[len(vs)-1] @@ -111,57 +156,22 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Type") == "application/json" { err := json.NewDecoder(r.Body).Decode(&in) if err != nil { - writeError(errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest) + writeError(path, in, w, errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest) return } } - w.Header().Add("Access-Control-Allow-Origin", "*") - //echo back headers client needs - reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers") - w.Header().Add("Access-Control-Allow-Headers", reqAccessHeaders) - - switch r.Method { - case "POST": - s.handlePost(w, r, path, in) - case "OPTIONS": - s.handleOptions(w, r, in) - default: - writeError(errors.Errorf("method %q not allowed - POST or OPTIONS required", r.Method), http.StatusMethodNotAllowed) - return - } -} - -func (s *server) handlePost(w http.ResponseWriter, r *http.Request, path string, in Params) { - writeError := func(err error, status int) { - fs.Errorf(nil, "rc: %q: error: %v", path, err) - // Adjust the error return for some well known errors - switch errors.Cause(err) { - case fs.ErrorDirNotFound, fs.ErrorObjectNotFound: - status = http.StatusNotFound - } - w.WriteHeader(status) - err = WriteJSON(w, Params{ - "error": err.Error(), - "input": in, - }) - if err != nil { - // can't return the error at this point - fs.Errorf(nil, "rc: failed to write JSON output: %v", err) - } - } - // Find the call call := registry.get(path) if call == nil { - writeError(errors.Errorf("couldn't find method %q", path), http.StatusMethodNotAllowed) + writeError(path, in, w, errors.Errorf("couldn't find method %q", path), http.StatusMethodNotAllowed) return } // Check to see if it is async or not isAsync, err := in.GetBool("_async") if err != nil { - writeError(err, http.StatusBadRequest) + writeError(path, in, w, err, http.StatusBadRequest) return } @@ -173,7 +183,7 @@ func (s *server) handlePost(w http.ResponseWriter, r *http.Request, path string, out, err = call.Fn(in) } if err != nil { - writeError(err, http.StatusInternalServerError) + writeError(path, in, w, err, http.StatusInternalServerError) return } if out == nil { @@ -187,6 +197,15 @@ func (s *server) handlePost(w http.ResponseWriter, r *http.Request, path string, fs.Errorf(nil, "rc: failed to write JSON output: %v", err) } } -func (s *server) handleOptions(w http.ResponseWriter, r *http.Request, in Params) { + +func (s *server) handleOptions(w http.ResponseWriter, r *http.Request, path string) { w.WriteHeader(http.StatusOK) } + +func (s *server) handleGet(w http.ResponseWriter, r *http.Request, path string) { + if s.files == nil { + w.WriteHeader(http.StatusNotFound) + return + } + s.files.ServeHTTP(w, r) +} diff --git a/fs/rc/rcflags/rcflags.go b/fs/rc/rcflags/rcflags.go index a8f921a9a..5440245af 100644 --- a/fs/rc/rcflags/rcflags.go +++ b/fs/rc/rcflags/rcflags.go @@ -16,5 +16,6 @@ var ( // AddFlags adds the remote control flags to the flagSet func AddFlags(flagSet *pflag.FlagSet) { flags.BoolVarP(flagSet, &Opt.Enabled, "rc", "", false, "Enable the remote control server.") + flags.StringVarP(flagSet, &Opt.Files, "rc-files", "", "", "Serve these files on the HTTP server.") httpflags.AddFlagsPrefix(flagSet, "rc-", &Opt.HTTPOptions) }