operations: add operations/getfile remote control API endpoint
Add operations/cat endpoint to the Remote Control (RC) API to allow reading and streaming file contents in-process over librclone / FFI and HTTP RC. Supports range options (offset, count, head, tail), separator, optional maxSize buffer limit, and returns both string and base64 encoded results.
This commit is contained in:
+43
-37
@@ -1313,50 +1313,16 @@ type readCloser struct {
|
|||||||
// if count >= 0 then only that many characters will be output
|
// if count >= 0 then only that many characters will be output
|
||||||
func Cat(ctx context.Context, f fs.Fs, w io.Writer, offset, count int64, sep []byte) error {
|
func Cat(ctx context.Context, f fs.Fs, w io.Writer, offset, count int64, sep []byte) error {
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
return ListFn(ctx, f, func(o fs.Object) {
|
return ListFn(ctx, f, func(o fs.Object) {
|
||||||
var err error
|
|
||||||
tr := accounting.Stats(ctx).NewTransfer(o, nil)
|
|
||||||
defer func() {
|
|
||||||
tr.Done(ctx, err)
|
|
||||||
}()
|
|
||||||
opt := fs.RangeOption{Start: offset, End: -1}
|
|
||||||
size := o.Size()
|
|
||||||
if opt.Start < 0 {
|
|
||||||
opt.Start += size
|
|
||||||
}
|
|
||||||
if count >= 0 {
|
|
||||||
opt.End = opt.Start + count - 1
|
|
||||||
}
|
|
||||||
var options []fs.OpenOption
|
|
||||||
if opt.Start > 0 || opt.End >= 0 {
|
|
||||||
options = append(options, &opt)
|
|
||||||
}
|
|
||||||
for _, option := range ci.DownloadHeaders {
|
|
||||||
options = append(options, option)
|
|
||||||
}
|
|
||||||
var in io.ReadCloser
|
|
||||||
in, err = Open(ctx, o, options...)
|
|
||||||
if err != nil {
|
|
||||||
err = fs.CountError(ctx, err)
|
|
||||||
fs.Errorf(o, "Failed to open: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if count >= 0 {
|
|
||||||
in = &readCloser{Reader: &io.LimitedReader{R: in, N: count}, Closer: in}
|
|
||||||
}
|
|
||||||
in = tr.Account(ctx, in).WithBuffer() // account and buffer the transfer
|
|
||||||
// take the lock just before we output stuff, so at the last possible moment
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
_, err = io.Copy(w, in)
|
err := catObject(ctx, o, w, offset, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fs.CountError(ctx, err)
|
|
||||||
fs.Errorf(o, "Failed to send to output: %v", err)
|
fs.Errorf(o, "Failed to send to output: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if len(sep) > 0 {
|
if len(sep) > 0 {
|
||||||
_, err = w.Write(sep)
|
if _, err := w.Write(sep); err != nil {
|
||||||
if err != nil {
|
|
||||||
err = fs.CountError(ctx, err)
|
err = fs.CountError(ctx, err)
|
||||||
fs.Errorf(o, "Failed to send separator to output: %v", err)
|
fs.Errorf(o, "Failed to send separator to output: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1364,6 +1330,46 @@ func Cat(ctx context.Context, f fs.Fs, w io.Writer, offset, count int64, sep []b
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// catObject sends an object or a range of it to the io.Writer
|
||||||
|
func catObject(ctx context.Context, o fs.Object, w io.Writer, offset, count int64) (err error) {
|
||||||
|
tr := accounting.Stats(ctx).NewTransfer(o, nil)
|
||||||
|
defer func() {
|
||||||
|
tr.Done(ctx, err)
|
||||||
|
}()
|
||||||
|
opt := fs.RangeOption{Start: offset, End: -1}
|
||||||
|
size := o.Size()
|
||||||
|
if opt.Start < 0 && size >= 0 {
|
||||||
|
opt.Start += size
|
||||||
|
}
|
||||||
|
if count >= 0 {
|
||||||
|
opt.End = opt.Start + count - 1
|
||||||
|
}
|
||||||
|
var options []fs.OpenOption
|
||||||
|
if opt.Start > 0 || opt.End >= 0 {
|
||||||
|
options = append(options, &opt)
|
||||||
|
}
|
||||||
|
for _, option := range fs.GetConfig(ctx).DownloadHeaders {
|
||||||
|
options = append(options, option)
|
||||||
|
}
|
||||||
|
var in io.ReadCloser
|
||||||
|
in, err = Open(ctx, o, options...)
|
||||||
|
if err != nil {
|
||||||
|
err = fs.CountError(ctx, err)
|
||||||
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
if count >= 0 {
|
||||||
|
in = &readCloser{Reader: &io.LimitedReader{R: in, N: count}, Closer: in}
|
||||||
|
}
|
||||||
|
in = tr.Account(ctx, in).WithBuffer() // account and buffer the transfer
|
||||||
|
defer fs.CheckClose(in, &err)
|
||||||
|
_, err = io.Copy(w, in)
|
||||||
|
if err != nil {
|
||||||
|
err = fs.CountError(ctx, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Rcat reads data from the Reader until EOF and uploads it to a file on remote
|
// Rcat reads data from the Reader until EOF and uploads it to a file on remote
|
||||||
//
|
//
|
||||||
// in is closed at the end of the transfer
|
// in is closed at the end of the transfer
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package operations
|
package operations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,11 +13,14 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config"
|
"github.com/rclone/rclone/fs/config"
|
||||||
|
"github.com/rclone/rclone/fs/fspath"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/fs/rc"
|
"github.com/rclone/rclone/fs/rc"
|
||||||
|
"github.com/rclone/rclone/fs/walk"
|
||||||
"github.com/rclone/rclone/lib/diskusage"
|
"github.com/rclone/rclone/lib/diskusage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1009,3 +1014,194 @@ func rcHashsumFile(ctx context.Context, in rc.Params) (out rc.Params, err error)
|
|||||||
}
|
}
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultGetFileMaxSize is the built-in maximum size limit in bytes for operations/getfile (2 MiB)
|
||||||
|
const DefaultGetFileMaxSize = 2 * 1024 * 1024
|
||||||
|
|
||||||
|
// maxLimitWriter writes into a buffer up to limit bytes, returning an error if more data is written.
|
||||||
|
type maxLimitWriter struct {
|
||||||
|
buf bytes.Buffer
|
||||||
|
limit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *maxLimitWriter) Write(p []byte) (n int, err error) {
|
||||||
|
rem := w.limit - int64(w.buf.Len())
|
||||||
|
if int64(len(p)) > rem {
|
||||||
|
if rem > 0 {
|
||||||
|
w.buf.Write(p[:rem])
|
||||||
|
}
|
||||||
|
return int(rem), fmt.Errorf("read exceeded maximum limit of %d bytes; use offset/count, maxSize or --rc-serve", w.limit)
|
||||||
|
}
|
||||||
|
return w.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "operations/getfile",
|
||||||
|
Fn: rcGetFile,
|
||||||
|
Title: "Get a single file into a JSON response.",
|
||||||
|
Help: `This takes the following parameters:
|
||||||
|
|
||||||
|
- fs - a remote name string e.g. "drive:path/to/file" or "drive:path/to/dir"
|
||||||
|
- remote - (optional) a path within that remote e.g. "file.txt"
|
||||||
|
- offset - (optional) start reading at offset N (or from end if negative) (default 0)
|
||||||
|
- count - (optional) only read N bytes (default -1)
|
||||||
|
- head - (optional) only read the first N bytes (default 0)
|
||||||
|
- tail - (optional) only read the last N bytes (default 0)
|
||||||
|
- maxSize - (optional) maximum size limit in bytes (can only lower the built-in maximum) (default 2097152 / 2MiB)
|
||||||
|
- base64 - (optional) if true, returns the file contents base64-encoded (default false)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
- result - contents of the file as string or base64-encoded string
|
||||||
|
|
||||||
|
For streaming file content over HTTP with Range support, use the --rc-serve flag instead.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a single file into a JSON response
|
||||||
|
func rcGetFile(ctx context.Context, in rc.Params) (out rc.Params, err error) {
|
||||||
|
remote, err := in.GetString("remote")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fsString, err := in.GetString("fs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if remote != "" {
|
||||||
|
in = in.Copy()
|
||||||
|
in["fs"] = fspath.JoinRootPath(fsString, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := in.GetInt64("offset")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
count, err := in.GetInt64("count")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if rc.IsErrParamNotFound(err) {
|
||||||
|
count = -1
|
||||||
|
}
|
||||||
|
head, err := in.GetInt64("head")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tail, err := in.GetInt64("tail")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
usedOffset := offset != 0 || count >= 0
|
||||||
|
usedHead := head > 0
|
||||||
|
usedTail := tail > 0
|
||||||
|
|
||||||
|
if (usedHead && usedTail) || (usedHead && usedOffset) || (usedTail && usedOffset) {
|
||||||
|
return nil, errors.New("can only use one of head, tail or offset/count")
|
||||||
|
}
|
||||||
|
if head < 0 {
|
||||||
|
return nil, errors.New("head cannot be negative")
|
||||||
|
}
|
||||||
|
if tail < 0 {
|
||||||
|
return nil, errors.New("tail cannot be negative")
|
||||||
|
}
|
||||||
|
if head > 0 {
|
||||||
|
offset = 0
|
||||||
|
count = head
|
||||||
|
}
|
||||||
|
if tail > 0 {
|
||||||
|
offset = -tail
|
||||||
|
count = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := int64(DefaultGetFileMaxSize)
|
||||||
|
maxSize, err := in.GetInt64("maxSize")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !rc.IsErrParamNotFound(err) {
|
||||||
|
if maxSize <= 0 {
|
||||||
|
return nil, errors.New("maxSize must be greater than 0")
|
||||||
|
}
|
||||||
|
if maxSize > DefaultGetFileMaxSize {
|
||||||
|
return nil, fmt.Errorf("maxSize %d cannot exceed built-in maximum of %d bytes", maxSize, DefaultGetFileMaxSize)
|
||||||
|
}
|
||||||
|
limit = maxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Encoded, err := in.GetBool("base64")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, f, err := rc.GetFsNamedFileOK(ctx, in, "fs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ci := fs.GetConfig(ctx)
|
||||||
|
var (
|
||||||
|
fileCount int
|
||||||
|
w = &maxLimitWriter{limit: limit}
|
||||||
|
)
|
||||||
|
|
||||||
|
err = walk.ListR(ctx, f, "", false, ci.MaxDepth, walk.ListObjects, func(entries fs.DirEntries) error {
|
||||||
|
for _, entry := range entries {
|
||||||
|
o, ok := entry.(fs.Object)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileCount++
|
||||||
|
if fileCount > 1 {
|
||||||
|
return errors.New("found more than one file; operations/getfile only supports a single file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-check size if known
|
||||||
|
size := o.Size()
|
||||||
|
if size >= 0 {
|
||||||
|
start := offset
|
||||||
|
if start < 0 {
|
||||||
|
start += size
|
||||||
|
}
|
||||||
|
if start > size {
|
||||||
|
start = size
|
||||||
|
}
|
||||||
|
reqSize := size - start
|
||||||
|
if count >= 0 && count < reqSize {
|
||||||
|
reqSize = count
|
||||||
|
}
|
||||||
|
if reqSize > limit {
|
||||||
|
return fmt.Errorf("requested size (%d bytes) exceeds maximum limit of %d bytes; use offset/count, maxSize or --rc-serve", reqSize, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = catObject(ctx, o, w, offset, count)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fileCount == 0 {
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
data := w.buf.Bytes()
|
||||||
|
if base64Encoded {
|
||||||
|
return rc.Params{
|
||||||
|
"result": base64.StdEncoding.EncodeToString(data),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if !utf8.Valid(data) {
|
||||||
|
return nil, errors.New("file content is not valid UTF-8 (use base64=true for binary files)")
|
||||||
|
}
|
||||||
|
return rc.Params{
|
||||||
|
"result": string(data),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package operations_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -893,3 +894,210 @@ func TestRcHashsumFile(t *testing.T) {
|
|||||||
assert.Equal(t, "md5", out["hashType"])
|
assert.Equal(t, "md5", out["hashType"])
|
||||||
assert.Equal(t, "0ef726ce9b1a7692357ff70dd321d595", out["hash"])
|
assert.Equal(t, "0ef726ce9b1a7692357ff70dd321d595", out["hash"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// operations/getfile: get a single file
|
||||||
|
func TestRcGetFile(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r, call := rcNewRun(t, "operations/getfile")
|
||||||
|
r.Mkdir(ctx, r.Fremote)
|
||||||
|
|
||||||
|
file1Contents := "Hello Rclone GetFile Operation!"
|
||||||
|
file1 := r.WriteBoth(ctx, "getfile-file1.txt", file1Contents, t1)
|
||||||
|
r.CheckLocalItems(t, file1)
|
||||||
|
r.CheckRemoteItems(t, file1)
|
||||||
|
|
||||||
|
// 1. Basic test with fs + remote
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, file1Contents, out["result"])
|
||||||
|
|
||||||
|
// 2. Combined fs parameter
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": path.Join(r.FremoteName, file1.Path),
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, file1Contents, out["result"])
|
||||||
|
|
||||||
|
// 3. Range offset and count
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"offset": int64(6),
|
||||||
|
"count": int64(6),
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Rclone", out["result"])
|
||||||
|
|
||||||
|
// 4. Head flag
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"head": int64(5),
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Hello", out["result"])
|
||||||
|
|
||||||
|
// 5. Tail flag
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"tail": int64(10),
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Operation!", out["result"])
|
||||||
|
|
||||||
|
// 6. Negative offset
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"offset": int64(-10),
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Operation!", out["result"])
|
||||||
|
|
||||||
|
// 7. Base64 encoding for text file
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"base64": true,
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(out["result"].(string))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, file1Contents, string(decoded))
|
||||||
|
|
||||||
|
// 8. Binary file: UTF-8 rejection vs base64 success
|
||||||
|
binaryData := "\x00\xff\xfe\xfd\x80\x81"
|
||||||
|
binFile := r.WriteBoth(ctx, "binary.dat", binaryData, t1)
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": binFile.Path,
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not valid UTF-8")
|
||||||
|
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": binFile.Path,
|
||||||
|
"base64": true,
|
||||||
|
}
|
||||||
|
out, err = call.Fn(ctx, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
decodedBin, err := base64.StdEncoding.DecodeString(out["result"].(string))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte(binaryData), decodedBin)
|
||||||
|
|
||||||
|
// 9. Max size limit violation test
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"maxSize": int64(5),
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "exceeds maximum limit")
|
||||||
|
|
||||||
|
// 10. Max size exceeding built-in limit
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"maxSize": int64(operations.DefaultGetFileMaxSize + 1),
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "cannot exceed built-in maximum")
|
||||||
|
|
||||||
|
// 11. Invalid maxSize values (<= 0)
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"maxSize": int64(0),
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "maxSize must be greater than 0")
|
||||||
|
|
||||||
|
in["maxSize"] = int64(-5)
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "maxSize must be greater than 0")
|
||||||
|
|
||||||
|
// 12. Invalid parameter types
|
||||||
|
for _, badParam := range []rc.Params{
|
||||||
|
{"fs": r.FremoteName, "remote": file1.Path, "offset": "not_an_int"},
|
||||||
|
{"fs": r.FremoteName, "remote": file1.Path, "count": "not_an_int"},
|
||||||
|
{"fs": r.FremoteName, "remote": file1.Path, "head": "not_an_int"},
|
||||||
|
{"fs": r.FremoteName, "remote": file1.Path, "tail": "not_an_int"},
|
||||||
|
{"fs": r.FremoteName, "remote": file1.Path, "maxSize": "not_an_int"},
|
||||||
|
{"fs": r.FremoteName, "remote": file1.Path, "base64": "not_a_bool"},
|
||||||
|
{"fs": r.FremoteName, "remote": 12345},
|
||||||
|
} {
|
||||||
|
_, err = call.Fn(ctx, badParam)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. Conflicting and invalid bounds
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"head": int64(5),
|
||||||
|
"tail": int64(5),
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "can only use one of head, tail or offset/count")
|
||||||
|
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"head": int64(-1),
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "head cannot be negative")
|
||||||
|
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": file1.Path,
|
||||||
|
"tail": int64(-1),
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "tail cannot be negative")
|
||||||
|
|
||||||
|
// 14. Non-existent file / directory with 0 files
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "does_not_exist.txt",
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
emptyDir := path.Join(r.LocalName, "emptydir")
|
||||||
|
require.NoError(t, os.Mkdir(emptyDir, 0755))
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": emptyDir,
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, fs.ErrorObjectNotFound, err)
|
||||||
|
|
||||||
|
// 15. Directory containing multiple files
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
}
|
||||||
|
_, err = call.Fn(ctx, in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "found more than one file")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user