diff --git a/docs/content/rc.md b/docs/content/rc.md index 34de3aa05..fb6df1cae 100644 --- a/docs/content/rc.md +++ b/docs/content/rc.md @@ -203,8 +203,6 @@ Rather than rclone rc operations/list --json '{"fs": "/tmp", "remote": "test", "opt": {"showHash": true}}' ``` - - ## Special parameters 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 {{< rem autogenerated start "- run make rcdocs - don't edit here" >}} ### backend/command: Runs a backend command. {#backend-command} @@ -1155,17 +1176,18 @@ changed like this. 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}}' 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) - 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} diff --git a/fs/bwtimetable.go b/fs/bwtimetable.go index 86a330de5..f0056e15d 100644 --- a/fs/bwtimetable.go +++ b/fs/bwtimetable.go @@ -1,6 +1,7 @@ package fs import ( + "encoding/json" "fmt" "strconv" "strings" @@ -264,3 +265,19 @@ func (x BwTimetable) LimitAt(tt time.Time) BwTimeSlot { func (x BwTimetable) Type() string { 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) +} diff --git a/fs/bwtimetable_test.go b/fs/bwtimetable_test.go index 2121c5164..b4d6fb829 100644 --- a/fs/bwtimetable_test.go +++ b/fs/bwtimetable_test.go @@ -1,16 +1,16 @@ package fs import ( + "encoding/json" "testing" "time" - "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Check it satisfies the interface -var _ pflag.Value = (*BwTimetable)(nil) +var _ flagger = (*BwTimetable)(nil) func TestBwTimetableSet(t *testing.T) { for _, test := range []struct { @@ -464,3 +464,102 @@ func TestBwTimetableLimitAt(t *testing.T) { 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)) + } +} diff --git a/fs/cutoffmode.go b/fs/cutoffmode.go index 19ec2b0c4..359143ee7 100644 --- a/fs/cutoffmode.go +++ b/fs/cutoffmode.go @@ -47,3 +47,14 @@ func (m *CutoffMode) Set(s string) error { func (m *CutoffMode) Type() 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 + }) +} diff --git a/fs/cutoffmode_test.go b/fs/cutoffmode_test.go index 0fb7cc562..2b20b71c8 100644 --- a/fs/cutoffmode_test.go +++ b/fs/cutoffmode_test.go @@ -1,6 +1,76 @@ 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 -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) + } +} diff --git a/fs/dump.go b/fs/dump.go index 5d89b5abd..ec10b9259 100644 --- a/fs/dump.go +++ b/fs/dump.go @@ -91,3 +91,11 @@ func (f *DumpFlags) Set(s string) error { func (f *DumpFlags) Type() string { 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 + }) +} diff --git a/fs/dump_test.go b/fs/dump_test.go index c3f046055..c0ea248dd 100644 --- a/fs/dump_test.go +++ b/fs/dump_test.go @@ -1,14 +1,15 @@ package fs import ( + "encoding/json" + "strconv" "testing" - "github.com/spf13/pflag" "github.com/stretchr/testify/assert" ) // Check it satisfies the interface -var _ pflag.Value = (*DumpFlags)(nil) +var _ flagger = (*DumpFlags)(nil) func TestDumpFlagsString(t *testing.T) { assert.Equal(t, "", DumpFlags(0).String()) @@ -56,3 +57,39 @@ func TestDumpFlagsType(t *testing.T) { f := DumpFlags(0) 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) + } + } + } +} diff --git a/fs/fs_test.go b/fs/fs_test.go index 8e2086411..4de8ba370 100644 --- a/fs/fs_test.go +++ b/fs/fs_test.go @@ -10,13 +10,13 @@ import ( "testing" "time" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" "github.com/pkg/errors" "github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/fserrors" "github.com/rclone/rclone/lib/pacer" - "github.com/spf13/pflag" "github.com/stretchr/testify/assert" ) diff --git a/fs/log.go b/fs/log.go index 1ded32694..f2c3a354a 100644 --- a/fs/log.go +++ b/fs/log.go @@ -69,6 +69,17 @@ func (l *LogLevel) Type() 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 var LogPrint = func(level LogLevel, text string) { text = fmt.Sprintf("%-6s: %s", level, text) diff --git a/fs/log_test.go b/fs/log_test.go index b3bddcef3..dcccb8043 100644 --- a/fs/log_test.go +++ b/fs/log_test.go @@ -1,15 +1,17 @@ package fs import ( + "encoding/json" "fmt" + "strconv" "testing" - "github.com/spf13/pflag" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Check it satisfies the interface -var _ pflag.Value = (*LogLevel)(nil) +var _ flagger = (*LogLevel)(nil) var _ fmt.Stringer = LogValueItem{} type withString struct{} @@ -26,3 +28,65 @@ func TestLogValue(t *testing.T) { x = LogValueHide("x", withString{}) 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) + } +} diff --git a/fs/parseduration.go b/fs/parseduration.go index b5104cc6c..7a572ed07 100644 --- a/fs/parseduration.go +++ b/fs/parseduration.go @@ -196,6 +196,14 @@ func (d Duration) Type() string { 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 func (d *Duration) Scan(s fmt.ScanState, ch rune) error { token, err := s.Token(true, nil) diff --git a/fs/parseduration_test.go b/fs/parseduration_test.go index 69de520ca..f4be5a9bc 100644 --- a/fs/parseduration_test.go +++ b/fs/parseduration_test.go @@ -1,18 +1,18 @@ package fs import ( + "encoding/json" "fmt" "strings" "testing" "time" - "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Check it satisfies the interface -var _ pflag.Value = (*Duration)(nil) +var _ flagger = (*Duration)(nil) func TestParseDuration(t *testing.T) { 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, 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) + } +} diff --git a/fs/rc/config.go b/fs/rc/config.go index 5add5b212..80edc891e 100644 --- a/fs/rc/config.go +++ b/fs/rc/config.go @@ -89,17 +89,18 @@ changed like this. 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}}' 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) - rclone rc options/set --json '{"main": {"LogLevel": 6}}' + rclone rc options/set --json '{"main": {"LogLevel": "NOTICE"}}' `, }) } diff --git a/fs/sizesuffix.go b/fs/sizesuffix.go index c1d1b79c9..22412ebbf 100644 --- a/fs/sizesuffix.go +++ b/fs/sizesuffix.go @@ -2,6 +2,7 @@ package fs // SizeSuffix is parsed by flag with k/M/G suffixes import ( + "encoding/json" "fmt" "math" "sort" @@ -143,3 +144,30 @@ func (l SizeSuffixList) Less(i, j int) bool { return l[i] < l[j] } func (l SizeSuffixList) Sort() { 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 + }) +} diff --git a/fs/sizesuffix_test.go b/fs/sizesuffix_test.go index 4e820dd17..2497a1700 100644 --- a/fs/sizesuffix_test.go +++ b/fs/sizesuffix_test.go @@ -1,6 +1,7 @@ package fs import ( + "encoding/json" "fmt" "testing" @@ -9,8 +10,15 @@ import ( "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 -var _ pflag.Value = (*SizeSuffix)(nil) +var _ flagger = (*SizeSuffix)(nil) func TestSizeSuffixString(t *testing.T) { for _, test := range []struct { @@ -102,3 +110,37 @@ func TestSizeSuffixScan(t *testing.T) { assert.Equal(t, 1, n) 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)) + } +} diff --git a/vfs/vfscommon/cachemode.go b/vfs/vfscommon/cachemode.go index 2a4f87c9d..38b6fec99 100644 --- a/vfs/vfscommon/cachemode.go +++ b/vfs/vfscommon/cachemode.go @@ -3,6 +3,7 @@ package vfscommon import ( "fmt" + "github.com/rclone/rclone/fs" "github.com/rclone/rclone/lib/errors" ) @@ -47,3 +48,14 @@ func (l *CacheMode) Set(s string) error { func (l *CacheMode) Type() string { 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 + }) +} diff --git a/vfs/vfscommon/cachemode_test.go b/vfs/vfscommon/cachemode_test.go index 04c3a8119..f36c9ad2e 100644 --- a/vfs/vfscommon/cachemode_test.go +++ b/vfs/vfscommon/cachemode_test.go @@ -1,6 +1,8 @@ package vfscommon import ( + "encoding/json" + "strconv" "testing" "github.com/spf13/pflag" @@ -10,6 +12,9 @@ import ( // Check CacheMode it satisfies the pflag interface var _ pflag.Value = (*CacheMode)(nil) +// Check CacheMode it satisfies the json.Unmarshaller interface +var _ json.Unmarshaler = (*CacheMode)(nil) + func TestCacheModeString(t *testing.T) { assert.Equal(t, "off", CacheModeOff.String()) assert.Equal(t, "full", CacheModeFull.String()) @@ -34,3 +39,27 @@ func TestCacheModeType(t *testing.T) { var m CacheMode 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") +}