From ae3963e4b44f531d3b30ba690b9d462ab372b252 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Fri, 11 Dec 2020 17:48:09 +0000 Subject: [PATCH] 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. --- docs/content/rc.md | 32 ++++++++-- fs/bwtimetable.go | 17 ++++++ fs/bwtimetable_test.go | 103 +++++++++++++++++++++++++++++++- fs/cutoffmode.go | 11 ++++ fs/cutoffmode_test.go | 74 ++++++++++++++++++++++- fs/dump.go | 8 +++ fs/dump_test.go | 41 ++++++++++++- fs/fs_test.go | 2 +- fs/log.go | 11 ++++ fs/log_test.go | 68 ++++++++++++++++++++- fs/parseduration.go | 8 +++ fs/parseduration_test.go | 41 ++++++++++++- fs/rc/config.go | 7 ++- fs/sizesuffix.go | 28 +++++++++ fs/sizesuffix_test.go | 44 +++++++++++++- vfs/vfscommon/cachemode.go | 12 ++++ vfs/vfscommon/cachemode_test.go | 29 +++++++++ 17 files changed, 516 insertions(+), 20 deletions(-) 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") +}