fs: Add string alternatives for setting options over the rc

Before this change options were read and set in native format. This
means for example nanoseconds for durations or an integer for
enumerated types, which isn't very convenient for humans.

This change enables these types to be set with a string with the
syntax as used in the command line instead, so `"10s"` rather than
`10000000000` or `"DEBUG"` rather than `8` for log level.
This commit is contained in:
Nick Craig-Wood 2020-12-11 17:48:09 +00:00
parent e32f08f37b
commit ae3963e4b4
17 changed files with 516 additions and 20 deletions

View File

@ -203,8 +203,6 @@ Rather than
rclone rc operations/list --json '{"fs": "/tmp", "remote": "test", "opt": {"showHash": true}}' rclone rc operations/list --json '{"fs": "/tmp", "remote": "test", "opt": {"showHash": true}}'
``` ```
## Special parameters ## Special parameters
The rc interface supports some special parameters which apply to The rc interface supports some special parameters which apply to
@ -294,6 +292,29 @@ $ rclone rc --json '{ "group": "job/1" }' core/stats
} }
``` ```
## Data types
When the API returns types, these will mostly be straight forward
integer, string or boolean types.
However some of the types returned by the [options/get](#options-get)
call and taken by the [options/set](#options-set) calls as well as the
`vfsOpt` and the `mountOpt` are as follows:
- `Duration` - these are returned as an integer duration in
nanoseconds. They may be set as an integer, or they may be set with
time string, eg "5s". See the [options section](/docs/#options) for
more info.
- `Size` - these are returned as an integer number of bytes. They may
be set as an integer or they may be set with a size suffix string,
eg "10M". See the [options section](/docs/#options) for more info.
- Enumerated type (such as `CutoffMode`, `DumpFlags`, `LogLevel`,
`VfsCacheMode` - these will be returned as an integer and may be set
as an integer but more conveniently they can be set as a string, eg
"HARD" for `CutoffMode` or `DEBUG` for `LogLevel`.
- `BandwidthSpec` - this will be set and returned as a string, eg
"1M".
## Supported commands ## Supported commands
{{< rem autogenerated start "- run make rcdocs - don't edit here" >}} {{< rem autogenerated start "- run make rcdocs - don't edit here" >}}
### backend/command: Runs a backend command. {#backend-command} ### backend/command: Runs a backend command. {#backend-command}
@ -1155,17 +1176,18 @@ changed like this.
For example: For example:
This sets DEBUG level logs (-vv) This sets DEBUG level logs (-vv) (these can be set by number or string)
rclone rc options/set --json '{"main": {"LogLevel": "DEBUG"}}'
rclone rc options/set --json '{"main": {"LogLevel": 8}}' rclone rc options/set --json '{"main": {"LogLevel": 8}}'
And this sets INFO level logs (-v) And this sets INFO level logs (-v)
rclone rc options/set --json '{"main": {"LogLevel": 7}}' rclone rc options/set --json '{"main": {"LogLevel": "INFO"}}'
And this sets NOTICE level logs (normal without -v) And this sets NOTICE level logs (normal without -v)
rclone rc options/set --json '{"main": {"LogLevel": 6}}' rclone rc options/set --json '{"main": {"LogLevel": "NOTICE"}}'
### pluginsctl/addPlugin: Add a plugin using url {#pluginsctl-addPlugin} ### pluginsctl/addPlugin: Add a plugin using url {#pluginsctl-addPlugin}

View File

@ -1,6 +1,7 @@
package fs package fs
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -264,3 +265,19 @@ func (x BwTimetable) LimitAt(tt time.Time) BwTimeSlot {
func (x BwTimetable) Type() string { func (x BwTimetable) Type() string {
return "BwTimetable" return "BwTimetable"
} }
// UnmarshalJSON unmarshals a string value
func (x *BwTimetable) UnmarshalJSON(in []byte) error {
var s string
err := json.Unmarshal(in, &s)
if err != nil {
return err
}
return x.Set(s)
}
// MarshalJSON marshals as a string value
func (x BwTimetable) MarshalJSON() ([]byte, error) {
s := x.String()
return json.Marshal(s)
}

View File

@ -1,16 +1,16 @@
package fs package fs
import ( import (
"encoding/json"
"testing" "testing"
"time" "time"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Check it satisfies the interface // Check it satisfies the interface
var _ pflag.Value = (*BwTimetable)(nil) var _ flagger = (*BwTimetable)(nil)
func TestBwTimetableSet(t *testing.T) { func TestBwTimetableSet(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
@ -464,3 +464,102 @@ func TestBwTimetableLimitAt(t *testing.T) {
assert.Equal(t, test.want, slot) assert.Equal(t, test.want, slot)
} }
} }
func TestBwTimetableUnmarshalJSON(t *testing.T) {
for _, test := range []struct {
in string
want BwTimetable
err bool
}{
{
`"Mon-10:20,bad"`,
BwTimetable(nil),
true,
},
{
`"0"`,
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 0, Bandwidth: BwPair{Tx: 0, Rx: 0}},
},
false,
},
{
`"666"`,
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 0, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
},
false,
},
{
`"666:333"`,
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 0, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 333 * 1024}},
},
false,
},
{
`"10:20,666"`,
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 1, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 2, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 3, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 4, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 5, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 6, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
},
false,
},
} {
var bwt BwTimetable
err := json.Unmarshal([]byte(test.in), &bwt)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, bwt)
}
}
func TestBwTimetableMarshalJSON(t *testing.T) {
for _, test := range []struct {
in BwTimetable
want string
}{
{
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 0, Bandwidth: BwPair{Tx: 0, Rx: 0}},
},
`"0"`,
},
{
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 0, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
},
`"666k"`,
},
{
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 0, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 333 * 1024}},
},
`"666k:333k"`,
},
{
BwTimetable{
BwTimeSlot{DayOfTheWeek: 0, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 1, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 2, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 3, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 4, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 5, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
BwTimeSlot{DayOfTheWeek: 6, HHMM: 1020, Bandwidth: BwPair{Tx: 666 * 1024, Rx: 666 * 1024}},
},
`"Sun-10:20,666k Mon-10:20,666k Tue-10:20,666k Wed-10:20,666k Thu-10:20,666k Fri-10:20,666k Sat-10:20,666k"`,
},
} {
got, err := json.Marshal(test.in)
require.NoError(t, err, test.want)
assert.Equal(t, test.want, string(got))
}
}

View File

@ -47,3 +47,14 @@ func (m *CutoffMode) Set(s string) error {
func (m *CutoffMode) Type() string { func (m *CutoffMode) Type() string {
return "string" return "string"
} }
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (m *CutoffMode) UnmarshalJSON(in []byte) error {
return UnmarshalJSONFlag(in, m, func(i int64) error {
if i < 0 || i >= int64(len(cutoffModeToString)) {
return errors.Errorf("Out of range cutoff mode %d", i)
}
*m = (CutoffMode)(i)
return nil
})
}

View File

@ -1,6 +1,76 @@
package fs package fs
import "github.com/spf13/pflag" import (
"encoding/json"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Check it satisfies the interface // Check it satisfies the interface
var _ pflag.Value = (*CutoffMode)(nil) var _ flagger = (*CutoffMode)(nil)
func TestCutoffModeString(t *testing.T) {
for _, test := range []struct {
in CutoffMode
want string
}{
{CutoffModeHard, "HARD"},
{CutoffModeSoft, "SOFT"},
{99, "CutoffMode(99)"},
} {
cm := test.in
got := cm.String()
assert.Equal(t, test.want, got, test.in)
}
}
func TestCutoffModeSet(t *testing.T) {
for _, test := range []struct {
in string
want CutoffMode
err bool
}{
{"hard", CutoffModeHard, false},
{"SOFT", CutoffModeSoft, false},
{"Cautious", CutoffModeCautious, false},
{"Potato", 0, true},
} {
cm := CutoffMode(0)
err := cm.Set(test.in)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, cm, test.in)
}
}
func TestCutoffModeUnmarshalJSON(t *testing.T) {
for _, test := range []struct {
in string
want CutoffMode
err bool
}{
{`"hard"`, CutoffModeHard, false},
{`"SOFT"`, CutoffModeSoft, false},
{`"Cautious"`, CutoffModeCautious, false},
{`"Potato"`, 0, true},
{strconv.Itoa(int(CutoffModeHard)), CutoffModeHard, false},
{strconv.Itoa(int(CutoffModeSoft)), CutoffModeSoft, false},
{`99`, 0, true},
{`-99`, 0, true},
} {
var cm CutoffMode
err := json.Unmarshal([]byte(test.in), &cm)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, cm, test.in)
}
}

View File

@ -91,3 +91,11 @@ func (f *DumpFlags) Set(s string) error {
func (f *DumpFlags) Type() string { func (f *DumpFlags) Type() string {
return "DumpFlags" return "DumpFlags"
} }
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (f *DumpFlags) UnmarshalJSON(in []byte) error {
return UnmarshalJSONFlag(in, f, func(i int64) error {
*f = (DumpFlags)(i)
return nil
})
}

View File

@ -1,14 +1,15 @@
package fs package fs
import ( import (
"encoding/json"
"strconv"
"testing" "testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// Check it satisfies the interface // Check it satisfies the interface
var _ pflag.Value = (*DumpFlags)(nil) var _ flagger = (*DumpFlags)(nil)
func TestDumpFlagsString(t *testing.T) { func TestDumpFlagsString(t *testing.T) {
assert.Equal(t, "", DumpFlags(0).String()) assert.Equal(t, "", DumpFlags(0).String())
@ -56,3 +57,39 @@ func TestDumpFlagsType(t *testing.T) {
f := DumpFlags(0) f := DumpFlags(0)
assert.Equal(t, "DumpFlags", f.Type()) assert.Equal(t, "DumpFlags", f.Type())
} }
func TestDumpFlagsUnmarshallJSON(t *testing.T) {
for _, test := range []struct {
in string
want DumpFlags
wantErr string
}{
{`""`, DumpFlags(0), ""},
{`"bodies"`, DumpBodies, ""},
{`"bodies,headers,auth"`, DumpBodies | DumpHeaders | DumpAuth, ""},
{`"bodies,headers,auth"`, DumpBodies | DumpHeaders | DumpAuth, ""},
{`"headers,bodies,requests,responses,auth,filters"`, DumpHeaders | DumpBodies | DumpRequests | DumpResponses | DumpAuth | DumpFilters, ""},
{`"headers,bodies,unknown,auth"`, 0, "Unknown dump flag \"unknown\""},
{`0`, DumpFlags(0), ""},
{strconv.Itoa(int(DumpBodies)), DumpBodies, ""},
{strconv.Itoa(int(DumpBodies | DumpHeaders | DumpAuth)), DumpBodies | DumpHeaders | DumpAuth, ""},
} {
f := DumpFlags(-1)
initial := f
err := json.Unmarshal([]byte(test.in), &f)
if err != nil {
if test.wantErr == "" {
t.Errorf("Got an error when not expecting one on %q: %v", test.in, err)
} else {
assert.Contains(t, err.Error(), test.wantErr)
}
assert.Equal(t, initial, f, test.want)
} else {
if test.wantErr != "" {
t.Errorf("Got no error when expecting one on %q", test.in)
} else {
assert.Equal(t, test.want, f)
}
}
}
}

View File

@ -10,13 +10,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/fserrors" "github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/pacer"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -69,6 +69,17 @@ func (l *LogLevel) Type() string {
return "string" return "string"
} }
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (l *LogLevel) UnmarshalJSON(in []byte) error {
return UnmarshalJSONFlag(in, l, func(i int64) error {
if i < 0 || i >= int64(LogLevel(len(logLevelToString))) {
return errors.Errorf("Unknown log level %d", i)
}
*l = (LogLevel)(i)
return nil
})
}
// LogPrint sends the text to the logger of level // LogPrint sends the text to the logger of level
var LogPrint = func(level LogLevel, text string) { var LogPrint = func(level LogLevel, text string) {
text = fmt.Sprintf("%-6s: %s", level, text) text = fmt.Sprintf("%-6s: %s", level, text)

View File

@ -1,15 +1,17 @@
package fs package fs
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv"
"testing" "testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// Check it satisfies the interface // Check it satisfies the interface
var _ pflag.Value = (*LogLevel)(nil) var _ flagger = (*LogLevel)(nil)
var _ fmt.Stringer = LogValueItem{} var _ fmt.Stringer = LogValueItem{}
type withString struct{} type withString struct{}
@ -26,3 +28,65 @@ func TestLogValue(t *testing.T) {
x = LogValueHide("x", withString{}) x = LogValueHide("x", withString{})
assert.Equal(t, "", x.String()) assert.Equal(t, "", x.String())
} }
func TestLogLevelString(t *testing.T) {
for _, test := range []struct {
in LogLevel
want string
}{
{LogLevelEmergency, "EMERGENCY"},
{LogLevelDebug, "DEBUG"},
{99, "LogLevel(99)"},
} {
logLevel := test.in
got := logLevel.String()
assert.Equal(t, test.want, got, test.in)
}
}
func TestLogLevelSet(t *testing.T) {
for _, test := range []struct {
in string
want LogLevel
err bool
}{
{"EMERGENCY", LogLevelEmergency, false},
{"DEBUG", LogLevelDebug, false},
{"Potato", 100, true},
} {
logLevel := LogLevel(100)
err := logLevel.Set(test.in)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, logLevel, test.in)
}
}
func TestLogLevelUnmarshalJSON(t *testing.T) {
for _, test := range []struct {
in string
want LogLevel
err bool
}{
{`"EMERGENCY"`, LogLevelEmergency, false},
{`"DEBUG"`, LogLevelDebug, false},
{`"Potato"`, 100, true},
{strconv.Itoa(int(LogLevelEmergency)), LogLevelEmergency, false},
{strconv.Itoa(int(LogLevelDebug)), LogLevelDebug, false},
{"Potato", 100, true},
{`99`, 100, true},
{`-99`, 100, true},
} {
logLevel := LogLevel(100)
err := json.Unmarshal([]byte(test.in), &logLevel)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, logLevel, test.in)
}
}

View File

@ -196,6 +196,14 @@ func (d Duration) Type() string {
return "Duration" return "Duration"
} }
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (d *Duration) UnmarshalJSON(in []byte) error {
return UnmarshalJSONFlag(in, d, func(i int64) error {
*d = Duration(i)
return nil
})
}
// Scan implements the fmt.Scanner interface // Scan implements the fmt.Scanner interface
func (d *Duration) Scan(s fmt.ScanState, ch rune) error { func (d *Duration) Scan(s fmt.ScanState, ch rune) error {
token, err := s.Token(true, nil) token, err := s.Token(true, nil)

View File

@ -1,18 +1,18 @@
package fs package fs
import ( import (
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Check it satisfies the interface // Check it satisfies the interface
var _ pflag.Value = (*Duration)(nil) var _ flagger = (*Duration)(nil)
func TestParseDuration(t *testing.T) { func TestParseDuration(t *testing.T) {
now := time.Date(2020, 9, 5, 8, 15, 5, 250, time.UTC) now := time.Date(2020, 9, 5, 8, 15, 5, 250, time.UTC)
@ -149,3 +149,40 @@ func TestDurationScan(t *testing.T) {
assert.Equal(t, 1, n) assert.Equal(t, 1, n)
assert.Equal(t, Duration(17*60*time.Second), v) assert.Equal(t, Duration(17*60*time.Second), v)
} }
func TestParseUnmarshalJSON(t *testing.T) {
for _, test := range []struct {
in string
want time.Duration
err bool
}{
{`""`, 0, true},
{`"0"`, 0, false},
{`"1ms"`, time.Millisecond, false},
{`"1s"`, time.Second, false},
{`"1m"`, time.Minute, false},
{`"1h"`, time.Hour, false},
{`"1d"`, time.Hour * 24, false},
{`"1w"`, time.Hour * 24 * 7, false},
{`"1M"`, time.Hour * 24 * 30, false},
{`"1y"`, time.Hour * 24 * 365, false},
{`"off"`, time.Duration(DurationOff), false},
{`"error"`, 0, true},
{"0", 0, false},
{"1000000", time.Millisecond, false},
{"1000000000", time.Second, false},
{"60000000000", time.Minute, false},
{"3600000000000", time.Hour, false},
{"9223372036854775807", time.Duration(DurationOff), false},
{"error", 0, true},
} {
var duration Duration
err := json.Unmarshal([]byte(test.in), &duration)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, Duration(test.want), duration, test.in)
}
}

View File

@ -89,17 +89,18 @@ changed like this.
For example: For example:
This sets DEBUG level logs (-vv) This sets DEBUG level logs (-vv) (these can be set by number or string)
rclone rc options/set --json '{"main": {"LogLevel": "DEBUG"}}'
rclone rc options/set --json '{"main": {"LogLevel": 8}}' rclone rc options/set --json '{"main": {"LogLevel": 8}}'
And this sets INFO level logs (-v) And this sets INFO level logs (-v)
rclone rc options/set --json '{"main": {"LogLevel": 7}}' rclone rc options/set --json '{"main": {"LogLevel": "INFO"}}'
And this sets NOTICE level logs (normal without -v) And this sets NOTICE level logs (normal without -v)
rclone rc options/set --json '{"main": {"LogLevel": 6}}' rclone rc options/set --json '{"main": {"LogLevel": "NOTICE"}}'
`, `,
}) })
} }

View File

@ -2,6 +2,7 @@ package fs
// SizeSuffix is parsed by flag with k/M/G suffixes // SizeSuffix is parsed by flag with k/M/G suffixes
import ( import (
"encoding/json"
"fmt" "fmt"
"math" "math"
"sort" "sort"
@ -143,3 +144,30 @@ func (l SizeSuffixList) Less(i, j int) bool { return l[i] < l[j] }
func (l SizeSuffixList) Sort() { func (l SizeSuffixList) Sort() {
sort.Sort(l) sort.Sort(l)
} }
// UnmarshalJSONFlag unmarshals a JSON input for a flag. If the input
// is a string then it calls the Set method on the flag otherwise it
// calls the setInt function with a parsed int64.
func UnmarshalJSONFlag(in []byte, x interface{ Set(string) error }, setInt func(int64) error) error {
// Try to parse as string first
var s string
err := json.Unmarshal(in, &s)
if err == nil {
return x.Set(s)
}
// If that fails parse as integer
var i int64
err = json.Unmarshal(in, &i)
if err != nil {
return err
}
return setInt(i)
}
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (x *SizeSuffix) UnmarshalJSON(in []byte) error {
return UnmarshalJSONFlag(in, x, func(i int64) error {
*x = SizeSuffix(i)
return nil
})
}

View File

@ -1,6 +1,7 @@
package fs package fs
import ( import (
"encoding/json"
"fmt" "fmt"
"testing" "testing"
@ -9,8 +10,15 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Interface which flags must satisfy - only defined for _test.go
// since we don't want to pull in pflag here
type flagger interface {
pflag.Value
json.Unmarshaler
}
// Check it satisfies the interface // Check it satisfies the interface
var _ pflag.Value = (*SizeSuffix)(nil) var _ flagger = (*SizeSuffix)(nil)
func TestSizeSuffixString(t *testing.T) { func TestSizeSuffixString(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
@ -102,3 +110,37 @@ func TestSizeSuffixScan(t *testing.T) {
assert.Equal(t, 1, n) assert.Equal(t, 1, n)
assert.Equal(t, SizeSuffix(17<<20), v) assert.Equal(t, SizeSuffix(17<<20), v)
} }
func TestSizeSuffixUnmarshalJSON(t *testing.T) {
for _, test := range []struct {
in string
want int64
err bool
}{
{`"0"`, 0, false},
{`"102B"`, 102, false},
{`"1K"`, 1024, false},
{`"2.5"`, 1024 * 2.5, false},
{`"1M"`, 1024 * 1024, false},
{`"1.g"`, 1024 * 1024 * 1024, false},
{`"10G"`, 10 * 1024 * 1024 * 1024, false},
{`"off"`, -1, false},
{`""`, 0, true},
{`"1q"`, 0, true},
{`"-1K"`, 0, true},
{`0`, 0, false},
{`102`, 102, false},
{`1024`, 1024, false},
{`1000000000`, 1000000000, false},
{`1.1.1`, 0, true},
} {
var ss SizeSuffix
err := json.Unmarshal([]byte(test.in), &ss)
if test.err {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, int64(ss))
}
}

View File

@ -3,6 +3,7 @@ package vfscommon
import ( import (
"fmt" "fmt"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/errors" "github.com/rclone/rclone/lib/errors"
) )
@ -47,3 +48,14 @@ func (l *CacheMode) Set(s string) error {
func (l *CacheMode) Type() string { func (l *CacheMode) Type() string {
return "CacheMode" return "CacheMode"
} }
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (l *CacheMode) UnmarshalJSON(in []byte) error {
return fs.UnmarshalJSONFlag(in, l, func(i int64) error {
if i < 0 || i >= int64(len(cacheModeToString)) {
return errors.Errorf("Unknown cache mode level %d", i)
}
*l = CacheMode(i)
return nil
})
}

View File

@ -1,6 +1,8 @@
package vfscommon package vfscommon
import ( import (
"encoding/json"
"strconv"
"testing" "testing"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -10,6 +12,9 @@ import (
// Check CacheMode it satisfies the pflag interface // Check CacheMode it satisfies the pflag interface
var _ pflag.Value = (*CacheMode)(nil) var _ pflag.Value = (*CacheMode)(nil)
// Check CacheMode it satisfies the json.Unmarshaller interface
var _ json.Unmarshaler = (*CacheMode)(nil)
func TestCacheModeString(t *testing.T) { func TestCacheModeString(t *testing.T) {
assert.Equal(t, "off", CacheModeOff.String()) assert.Equal(t, "off", CacheModeOff.String())
assert.Equal(t, "full", CacheModeFull.String()) assert.Equal(t, "full", CacheModeFull.String())
@ -34,3 +39,27 @@ func TestCacheModeType(t *testing.T) {
var m CacheMode var m CacheMode
assert.Equal(t, "CacheMode", m.Type()) assert.Equal(t, "CacheMode", m.Type())
} }
func TestCacheModeUnmarshalJSON(t *testing.T) {
var m CacheMode
err := json.Unmarshal([]byte(`"full"`), &m)
assert.NoError(t, err)
assert.Equal(t, CacheModeFull, m)
err = json.Unmarshal([]byte(`"potato"`), &m)
assert.Error(t, err, "Unknown cache mode level")
err = json.Unmarshal([]byte(`""`), &m)
assert.Error(t, err, "Unknown cache mode level")
err = json.Unmarshal([]byte(strconv.Itoa(int(CacheModeFull))), &m)
assert.NoError(t, err)
assert.Equal(t, CacheModeFull, m)
err = json.Unmarshal([]byte("-1"), &m)
assert.Error(t, err, "Unknown cache mode level")
err = json.Unmarshal([]byte("99"), &m)
assert.Error(t, err, "Unknown cache mode level")
}