librclone: change interface for C code and add Mobile interface #4891

This changes the interface for the C code to return a struct on the
stack that is defined in the code rather than one which is defined by
the cgo compiler. This is more future proof and inline with the
gomobile interface.

This also adds a gomobile interface RcloneMobileRPC which uses generic
go types conforming to the gobind restrictions.

It also fixes up initialisation errors.
This commit is contained in:
Nick Craig-Wood 2021-03-30 10:02:48 +01:00
parent 5db88fed2b
commit f38c262471
2 changed files with 81 additions and 38 deletions

View File

@ -5,10 +5,10 @@
#include "librclone.h" #include "librclone.h"
void testRPC(char *method, char *in) { void testRPC(char *method, char *in) {
struct RcloneRPC_return out = RcloneRPC(method, in); struct RcloneRPCResult out = RcloneRPC(method, in);
printf("status: %d\n", out.r1); printf("status: %d\n", out.Status);
printf("output: %s\n", out.r0); printf("output: %s\n", out.Output);
free(out.r0); free(out.Output);
} }
// noop command // noop command

View File

@ -19,9 +19,15 @@
// The library will depend on `libdl` and `libpthread`. // The library will depend on `libdl` and `libpthread`.
package main package main
import ( /*
"C" struct RcloneRPCResult {
char* Output;
int Status;
};
*/
import "C"
import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -31,6 +37,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/log"
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/fs/rc/jobs" "github.com/rclone/rclone/fs/rc/jobs"
@ -42,7 +51,19 @@ import (
// //
//export RcloneInitialize //export RcloneInitialize
func RcloneInitialize() { func RcloneInitialize() {
// TODO: what need to be initialized manually? // A subset of initialisation copied from cmd.go
// Note that we don't want to pull in anything which depends on pflags
ctx := context.Background()
// Start the logger
log.InitLogging()
// Load the config - this may need to be configurable
configfile.Install()
// Start accounting
accounting.Start(ctx)
} }
// RcloneFinalize finalizes the library // RcloneFinalize finalizes the library
@ -54,50 +75,73 @@ func RcloneFinalize() {
runtime.GC() runtime.GC()
} }
// RcloneRPCResult is returned from RcloneRPC
//
// Output will be returned as a serialized JSON object
// Status is a HTTP status return (200=OK anything else fail)
type RcloneRPCResult struct {
Output *C.char
Status C.int
}
// RcloneRPC does a single RPC call. The inputs are (method, input) // RcloneRPC does a single RPC call. The inputs are (method, input)
// and the output is (output, status). This is an exported interface // and the output is (output, status). This is an exported interface
// to the rclone API as described in https://rclone.org/rc/ // to the rclone API as described in https://rclone.org/rc/
// //
// method is a string, eg "operations/list" // method is a string, eg "operations/list"
// input should be a serialized JSON object // input should be a serialized JSON object
// output will be returned as a serialized JSON object // result.Output will be returned as a serialized JSON object
// status is a HTTP status return (200=OK anything else fail) // result.Status is a HTTP status return (200=OK anything else fail)
// //
// Caller is responsible for freeing the memory for output // Caller is responsible for freeing the memory for result.Output,
// // result itself is passed on the stack.
// Note that when calling from C output and status are returned in an
// RcloneRPC_return which has two members r0 which is output and r1
// which is status.
// //
//export RcloneRPC //export RcloneRPC
func RcloneRPC(method *C.char, input *C.char) (output *C.char, status C.int) { //nolint:golint func RcloneRPC(method *C.char, input *C.char) (result C.struct_RcloneRPCResult) { //nolint:golint
res, s := callFunctionJSON(C.GoString(method), C.GoString(input)) output, status := callFunctionJSON(C.GoString(method), C.GoString(input))
return C.CString(res), C.int(s) result.Output = C.CString(output)
result.Status = C.int(status)
return result
}
// RcloneMobileRPCResult is returned from RcloneMobileRPC
//
// Output will be returned as a serialized JSON object
// Status is a HTTP status return (200=OK anything else fail)
type RcloneMobileRPCResult struct {
Output string
Status int
}
// RcloneMobileRPCRPC this works the same as RcloneRPC but has an interface
// optimised for gomobile, in particular the function signature is
// valid under gobind rules.
//
// https://pkg.go.dev/golang.org/x/mobile/cmd/gobind#hdr-Type_restrictions
func RcloneMobileRPCRPC(method string, input string) (result RcloneMobileRPCResult) {
output, status := callFunctionJSON(method, input)
result.Output = output
result.Status = status
return result
} }
// writeError returns a formatted error string and the status passed in // writeError returns a formatted error string and the status passed in
func writeError(path string, in rc.Params, err error, status int) (string, int) { func writeError(path string, in rc.Params, err error, status int) (string, int) {
fs.Errorf(nil, "rc: %q: error: %v", path, err) fs.Errorf(nil, "rc: %q: error: %v", path, err)
params, status := rc.Error(path, in, err, status)
var w strings.Builder var w strings.Builder
// FIXME should factor this err = rc.WriteJSON(&w, params)
// Adjust the error return for some well known errors
errOrig := errors.Cause(err)
switch {
case errOrig == fs.ErrorDirNotFound || errOrig == fs.ErrorObjectNotFound:
status = http.StatusNotFound
case rc.IsErrParamInvalid(err) || rc.IsErrParamNotFound(err):
status = http.StatusBadRequest
}
// w.WriteHeader(status)
err = rc.WriteJSON(&w, rc.Params{
"status": status,
"error": err.Error(),
"input": in,
"path": path,
})
if err != nil { if err != nil {
// can't return the error at this point // ultimate fallback error
return fmt.Sprintf(`{"error": "rc: failed to write JSON output: %v"}`, err), status fs.Errorf(nil, "writeError: failed to write JSON output from %#v: %v", in, err)
status = http.StatusInternalServerError
w.Reset()
fmt.Fprintf(&w, `{
"error": %q,
"path": %q,
"status": %d
}`, err, path, status)
} }
return w.String(), status return w.String(), status
} }
@ -110,7 +154,6 @@ func callFunctionJSON(method string, input string) (output string, status int) {
in := make(rc.Params) in := make(rc.Params)
err := json.NewDecoder(strings.NewReader(input)).Decode(&in) err := json.NewDecoder(strings.NewReader(input)).Decode(&in)
if err != nil { if err != nil {
// TODO: handle error
return writeError(method, in, errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest) return writeError(method, in, errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest)
} }
@ -132,10 +175,9 @@ func callFunctionJSON(method string, input string) (output string, status int) {
} }
fs.Debugf(nil, "rc: %q: with parameters %+v", method, in) fs.Debugf(nil, "rc: %q: with parameters %+v", method, in)
// TODO: what is r.Context()? use Background() for the moment
_, out, err := jobs.NewJob(context.Background(), call.Fn, in) _, out, err := jobs.NewJob(context.Background(), call.Fn, in)
if err != nil { if err != nil {
// handle error
return writeError(method, in, err, http.StatusInternalServerError) return writeError(method, in, err, http.StatusInternalServerError)
} }
if out == nil { if out == nil {
@ -150,6 +192,7 @@ func callFunctionJSON(method string, input string) (output string, status int) {
fs.Errorf(nil, "rc: failed to write JSON output: %v", err) fs.Errorf(nil, "rc: failed to write JSON output: %v", err)
return writeError(method, in, err, http.StatusInternalServerError) return writeError(method, in, err, http.StatusInternalServerError)
} }
return w.String(), http.StatusOK return w.String(), http.StatusOK
} }