rc: add --rc-files flag to serve files on the rc http server

This enables building a browser based UI for rclone
This commit is contained in:
Nick Craig-Wood 2018-10-27 16:00:31 +01:00
parent 2089405e1b
commit 75252e4a89
2 changed files with 75 additions and 55 deletions

View File

@ -10,6 +10,7 @@ package rc
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"mime"
"net/http" "net/http"
_ "net/http/pprof" // install the pprof http handlers _ "net/http/pprof" // install the pprof http handlers
"strings" "strings"
@ -17,12 +18,14 @@ import (
"github.com/ncw/rclone/cmd/serve/httplib" "github.com/ncw/rclone/cmd/serve/httplib"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/skratchdot/open-golang/open"
) )
// Options contains options for the remote control server // Options contains options for the remote control server
type Options struct { type Options struct {
HTTPOptions httplib.Options 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 // DefaultOpt is the default values used for Options
@ -45,7 +48,8 @@ func Start(opt *Options) {
// server contains everything to run the server // server contains everything to run the server
type server struct { type server struct {
srv *httplib.Server srv *httplib.Server
files http.Handler
} }
func newServer(opt *Options) *server { func newServer(opt *Options) *server {
@ -55,6 +59,17 @@ func newServer(opt *Options) *server {
srv: httplib.NewServer(mux, &opt.HTTPOptions), srv: httplib.NewServer(mux, &opt.HTTPOptions),
} }
mux.HandleFunc("/", s.handler) 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 return s
} }
@ -65,6 +80,10 @@ func (s *server) serve() {
fs.Errorf(nil, "Opening listener: %v", err) fs.Errorf(nil, "Opening listener: %v", err)
} }
fs.Logf(nil, "Serving remote control on %s", s.srv.URL()) 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() s.srv.Wait()
} }
@ -75,32 +94,58 @@ func WriteJSON(w io.Writer, out Params) error {
return enc.Encode(out) 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 // handler reads incoming requests and dispatches them
func (s *server) handler(w http.ResponseWriter, r *http.Request) { func (s *server) handler(w http.ResponseWriter, r *http.Request) {
path := strings.Trim(r.URL.Path, "/") path := strings.Trim(r.URL.Path, "/")
in := make(Params)
writeError := func(err error, status int) { w.Header().Add("Access-Control-Allow-Origin", "*")
fs.Errorf(nil, "rc: %q: error: %v", path, err)
w.WriteHeader(status) // echo back access control headers client needs
err = WriteJSON(w, Params{ reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers")
"error": err.Error(), w.Header().Add("Access-Control-Allow-Headers", reqAccessHeaders)
"input": in,
}) switch r.Method {
if err != nil { case "POST":
// can't return the error at this point s.handlePost(w, r, path)
fs.Errorf(nil, "rc: failed to write JSON output: %v", err) 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 // Parse the POST and URL parameters into r.Form, for others r.Form will be empty value
err := r.ParseForm() err := r.ParseForm()
if err != nil { 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 return
} }
// Read the POST and URL parameters into in // Read the POST and URL parameters into in
in := make(Params)
for k, vs := range r.Form { for k, vs := range r.Form {
if len(vs) > 0 { if len(vs) > 0 {
in[k] = vs[len(vs)-1] 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" { if r.Header.Get("Content-Type") == "application/json" {
err := json.NewDecoder(r.Body).Decode(&in) err := json.NewDecoder(r.Body).Decode(&in)
if err != nil { 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 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 // Find the call
call := registry.get(path) call := registry.get(path)
if call == nil { 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 return
} }
// Check to see if it is async or not // Check to see if it is async or not
isAsync, err := in.GetBool("_async") isAsync, err := in.GetBool("_async")
if err != nil { if err != nil {
writeError(err, http.StatusBadRequest) writeError(path, in, w, err, http.StatusBadRequest)
return return
} }
@ -173,7 +183,7 @@ func (s *server) handlePost(w http.ResponseWriter, r *http.Request, path string,
out, err = call.Fn(in) out, err = call.Fn(in)
} }
if err != nil { if err != nil {
writeError(err, http.StatusInternalServerError) writeError(path, in, w, err, http.StatusInternalServerError)
return return
} }
if out == nil { 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) 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) 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)
}

View File

@ -16,5 +16,6 @@ var (
// AddFlags adds the remote control flags to the flagSet // AddFlags adds the remote control flags to the flagSet
func AddFlags(flagSet *pflag.FlagSet) { func AddFlags(flagSet *pflag.FlagSet) {
flags.BoolVarP(flagSet, &Opt.Enabled, "rc", "", false, "Enable the remote control server.") 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) httpflags.AddFlagsPrefix(flagSet, "rc-", &Opt.HTTPOptions)
} }