mirror of
https://github.com/rclone/rclone.git
synced 2024-11-22 16:34:30 +01:00
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:
parent
2089405e1b
commit
75252e4a89
129
fs/rc/rc.go
129
fs/rc/rc.go
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user