diff --git a/lib/config/config.go b/lib/config/config.go
index 677ea721c..e31af4a03 100644
--- a/lib/config/config.go
+++ b/lib/config/config.go
@@ -29,7 +29,7 @@ import (
const (
OldestHandledVersion = 10
- CurrentVersion = 19
+ CurrentVersion = 20
MaxRescanIntervalS = 365 * 24 * 60 * 60
)
@@ -292,6 +292,9 @@ func (cfg *Configuration) clean() error {
if cfg.Version == 18 {
convertV18V19(cfg)
}
+ if cfg.Version == 19 {
+ convertV19V20(cfg)
+ }
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -341,6 +344,18 @@ func (cfg *Configuration) clean() error {
return nil
}
+func convertV19V20(cfg *Configuration) {
+ cfg.Options.MinHomeDiskFree = Size{Value: cfg.Options.DeprecatedMinHomeDiskFreePct, Unit: "%"}
+ cfg.Options.DeprecatedMinHomeDiskFreePct = 0
+
+ for i := range cfg.Folders {
+ cfg.Folders[i].MinDiskFree = Size{Value: cfg.Folders[i].DeprecatedMinDiskFreePct, Unit: "%"}
+ cfg.Folders[i].DeprecatedMinDiskFreePct = 0
+ }
+
+ cfg.Version = 20
+}
+
func convertV18V19(cfg *Configuration) {
// Triggers a database tweak
cfg.Version = 19
@@ -537,7 +552,7 @@ func convertV11V12(cfg *Configuration) {
func convertV10V11(cfg *Configuration) {
// Set minimum disk free of existing folders to 1%
for i := range cfg.Folders {
- cfg.Folders[i].MinDiskFreePct = 1
+ cfg.Folders[i].DeprecatedMinDiskFreePct = 1
}
cfg.Version = 11
}
diff --git a/lib/config/config_test.go b/lib/config/config_test.go
index c3131c791..c297d7a93 100644
--- a/lib/config/config_test.go
+++ b/lib/config/config_test.go
@@ -55,7 +55,7 @@ func TestDefaultValues(t *testing.T) {
CacheIgnoredFiles: false,
ProgressUpdateIntervalS: 5,
LimitBandwidthInLan: false,
- MinHomeDiskFreePct: 1,
+ MinHomeDiskFree: Size{1, "%"},
URURL: "https://data.syncthing.net/newdata",
URInitialDelayS: 1800,
URPostInsecurely: false,
@@ -110,7 +110,7 @@ func TestDeviceConfig(t *testing.T) {
Pullers: 0,
Hashers: 0,
AutoNormalize: true,
- MinDiskFreePct: 1,
+ MinDiskFree: Size{1, "%"},
MaxConflicts: -1,
Fsync: true,
Versioning: VersioningConfiguration{
@@ -201,7 +201,7 @@ func TestOverriddenValues(t *testing.T) {
CacheIgnoredFiles: true,
ProgressUpdateIntervalS: 10,
LimitBandwidthInLan: true,
- MinHomeDiskFreePct: 5.2,
+ MinHomeDiskFree: Size{5.2, "%"},
URURL: "https://localhost/newdata",
URInitialDelayS: 800,
URPostInsecurely: true,
diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go
index 2697ae035..76bda68dc 100644
--- a/lib/config/folderconfiguration.go
+++ b/lib/config/folderconfiguration.go
@@ -26,7 +26,7 @@ type FolderConfiguration struct {
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
- MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
+ MinDiskFree Size `xml:"minDiskFree" json:"minDiskFree"`
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
@@ -45,7 +45,8 @@ type FolderConfiguration struct {
cachedPath string
- DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
+ DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
+ DeprecatedMinDiskFreePct float64 `xml:"minDiskFreePct" json:"-"`
}
type FolderDeviceConfiguration struct {
diff --git a/lib/config/optionsconfiguration.go b/lib/config/optionsconfiguration.go
index bfd06b58c..a736d8a5a 100644
--- a/lib/config/optionsconfiguration.go
+++ b/lib/config/optionsconfiguration.go
@@ -123,7 +123,7 @@ type OptionsConfiguration struct {
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
- MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
+ MinHomeDiskFree Size `xml:"minHomeDiskFree" json:"minHomeDiskFree" default:"1 %"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
@@ -141,11 +141,12 @@ type OptionsConfiguration struct {
KCPSendWindowSize int `xml:"kcpSendWindowSize" json:"kcpSendWindowSize" default:"128"`
KCPReceiveWindowSize int `xml:"kcpReceiveWindowSize" json:"kcpReceiveWindowSize" default:"128"`
- DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
- DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`
- DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes,omitempty" json:"-"`
- DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds,omitempty" json:"-"`
- DeprecatedRelayServers []string `xml:"relayServer,omitempty" json:"-"`
+ DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
+ DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`
+ DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes,omitempty" json:"-"`
+ DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds,omitempty" json:"-"`
+ DeprecatedRelayServers []string `xml:"relayServer,omitempty" json:"-"`
+ DeprecatedMinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"-"`
}
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
diff --git a/lib/config/size.go b/lib/config/size.go
new file mode 100644
index 000000000..1628b9b11
--- /dev/null
+++ b/lib/config/size.go
@@ -0,0 +1,75 @@
+// Copyright (C) 2017 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package config
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type Size struct {
+ Value float64 `json:"value" xml:",chardata"`
+ Unit string `json:"unit" xml:"unit,attr"`
+}
+
+func ParseSize(s string) (Size, error) {
+ s = strings.TrimSpace(s)
+ if len(s) == 0 {
+ return Size{}, nil
+ }
+
+ var num, unit string
+ for i := 0; i < len(s) && (s[i] >= '0' && s[i] <= '9' || s[i] == '.' || s[i] == ','); i++ {
+ num = s[:i+1]
+ }
+ var i = len(num)
+ for i < len(s) && s[i] == ' ' {
+ i++
+ }
+ unit = s[i:]
+
+ val, err := strconv.ParseFloat(num, 64)
+ if err != nil {
+ return Size{}, err
+ }
+
+ return Size{val, unit}, nil
+}
+
+func (s Size) BaseValue() float64 {
+ unitPrefix := s.Unit
+ if len(unitPrefix) > 1 {
+ unitPrefix = unitPrefix[:1]
+ }
+
+ mult := 1.0
+ switch unitPrefix {
+ case "k", "K":
+ mult = 1000
+ case "m", "M":
+ mult = 1000 * 1000
+ case "g", "G":
+ mult = 1000 * 1000 * 1000
+ case "t", "T":
+ mult = 1000 * 1000 * 1000 * 1000
+ }
+
+ return s.Value * mult
+}
+
+func (s Size) Percentage() bool {
+ return strings.Contains(s.Unit, "%")
+}
+
+func (s Size) String() string {
+ return fmt.Sprintf("%v %s", s.Value, s.Unit)
+}
+
+func (Size) ParseDefault(s string) (interface{}, error) {
+ return ParseSize(s)
+}
diff --git a/lib/config/size_test.go b/lib/config/size_test.go
new file mode 100644
index 000000000..8e49a88b9
--- /dev/null
+++ b/lib/config/size_test.go
@@ -0,0 +1,72 @@
+// Copyright (C) 2017 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package config
+
+import "testing"
+
+func TestParseSize(t *testing.T) {
+ cases := []struct {
+ in string
+ ok bool
+ val float64
+ pct bool
+ }{
+ // We accept upper case SI units
+ {"5K", true, 5e3, false}, // even when they should be lower case
+ {"4 M", true, 4e6, false},
+ {"3G", true, 3e9, false},
+ {"2 T", true, 2e12, false},
+ // We accept lower case SI units out of user friendliness
+ {"1 k", true, 1e3, false},
+ {"2m", true, 2e6, false},
+ {"3 g", true, 3e9, false},
+ {"4t", true, 4e12, false},
+ // Fractions are OK
+ {"123.456 k", true, 123.456e3, false},
+ {"0.1234 m", true, 0.1234e6, false},
+ {"3.45 g", true, 3.45e9, false},
+ // We don't parse negative numbers
+ {"-1", false, 0, false},
+ {"-1k", false, 0, false},
+ {"-0.45g", false, 0, false},
+ // We accept various unit suffixes on the unit prefix
+ {"100 KBytes", true, 100e3, false},
+ {"100 Kbps", true, 100e3, false},
+ {"100 MAU", true, 100e6, false},
+ // Percentages are OK
+ {"1%", true, 1, true},
+ {"200%", true, 200, true}, // even large ones
+ {"200K%", true, 200e3, true}, // even with prefixes, although this makes no sense
+ {"2.34%", true, 2.34, true}, // fractions are A-ok
+ // The empty string is a valid zero
+ {"", true, 0, false},
+ {" ", true, 0, false},
+ }
+
+ for _, tc := range cases {
+ size, err := ParseSize(tc.in)
+
+ if !tc.ok {
+ if err == nil {
+ t.Errorf("Unexpected nil error in UnmarshalText(%q)", tc.in)
+ }
+ continue
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error in UnmarshalText(%q): %v", tc.in, err)
+ continue
+ }
+ if size.BaseValue() > tc.val*1.001 || size.BaseValue() < tc.val*0.999 {
+ // Allow 0.1% slop due to floating point multiplication
+ t.Errorf("Incorrect value in UnmarshalText(%q): %v, wanted %v", tc.in, size.BaseValue(), tc.val)
+ }
+ if size.Percentage() != tc.pct {
+ t.Errorf("Incorrect percentage bool in UnmarshalText(%q): %v, wanted %v", tc.in, size.Percentage(), tc.pct)
+ }
+ }
+}
diff --git a/lib/config/testdata/v20.xml b/lib/config/testdata/v20.xml
new file mode 100644
index 000000000..22eca7b5e
--- /dev/null
+++ b/lib/config/testdata/v20.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ 1
+ -1
+ true
+
+
+ tcp://a
+
+
+ tcp://b
+
+
diff --git a/lib/model/model.go b/lib/model/model.go
index f09401f9e..70fc0c016 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -109,8 +109,6 @@ var (
errFolderPathEmpty = errors.New("folder path empty")
errFolderPathMissing = errors.New("folder path missing")
errFolderMarkerMissing = errors.New("folder marker missing")
- errHomeDiskNoSpace = errors.New("home disk has insufficient free space")
- errFolderNoSpace = errors.New("folder has insufficient free space")
errInvalidFilename = errors.New("filename is invalid")
errDeviceUnknown = errors.New("unknown device")
errDevicePaused = errors.New("device is paused")
@@ -2298,29 +2296,31 @@ func (m *Model) checkFolderPath(folder config.FolderConfiguration) error {
// checkFolderFreeSpace returns nil if the folder has the required amount of
// free space, or if folder free space checking is disabled.
func (m *Model) checkFolderFreeSpace(folder config.FolderConfiguration) error {
- if folder.MinDiskFreePct <= 0 {
- return nil
- }
-
- free, err := osutil.DiskFreePercentage(folder.Path())
- if err == nil && free < folder.MinDiskFreePct {
- return errFolderNoSpace
- }
-
- return nil
+ return m.checkFreeSpace(folder.MinDiskFree, folder.Path())
}
// checkHomeDiskFree returns nil if the home disk has the required amount of
// free space, or if home disk free space checking is disabled.
func (m *Model) checkHomeDiskFree() error {
- minFree := m.cfg.Options().MinHomeDiskFreePct
- if minFree <= 0 {
+ return m.checkFreeSpace(m.cfg.Options().MinHomeDiskFree, m.cfg.ConfigPath())
+}
+
+func (m *Model) checkFreeSpace(req config.Size, path string) error {
+ val := req.BaseValue()
+ if val <= 0 {
return nil
}
- free, err := osutil.DiskFreePercentage(m.cfg.ConfigPath())
- if err == nil && free < minFree {
- return errHomeDiskNoSpace
+ if req.Percentage() {
+ free, err := osutil.DiskFreePercentage(path)
+ if err == nil && free < val {
+ return fmt.Errorf("insufficient space in %v: %f %% < %v", path, free, req)
+ }
+ } else {
+ free, err := osutil.DiskFreeBytes(path)
+ if err == nil && float64(free) < val {
+ return fmt.Errorf("insufficient space in %v: %v < %v", path, free, req)
+ }
}
return nil
diff --git a/lib/model/rwfolder.go b/lib/model/rwfolder.go
index 6fb073dcc..31419f7f5 100644
--- a/lib/model/rwfolder.go
+++ b/lib/model/rwfolder.go
@@ -1110,7 +1110,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
blocksSize = file.Size
}
- if f.MinDiskFreePct > 0 {
+ if f.MinDiskFree.BaseValue() > 0 {
if free, err := osutil.DiskFreeBytes(f.dir); err == nil && free < blocksSize {
l.Warnf(`Folder "%s": insufficient disk space in %s for %s: have %.2f MiB, need %.2f MiB`, f.folderID, f.dir, file.Name, float64(free)/1024/1024, float64(blocksSize)/1024/1024)
f.newError(file.Name, errors.New("insufficient space"))