diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 2dbbe5959..26faaaefb 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -1117,7 +1117,7 @@ func defaultConfig(myName string) config.Configuration { defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder]) defaultFolder.Label = "Default Folder" defaultFolder.RescanIntervalS = 60 - defaultFolder.MinDiskFreePct = 1 + defaultFolder.MinDiskFree = config.Size{Value: 1, Unit: "%"} defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}} defaultFolder.AutoNormalize = true defaultFolder.MaxConflicts = -1 diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json index 3f8513a54..b7f1333f4 100644 --- a/gui/default/assets/lang/lang-en.json +++ b/gui/default/assets/lang/lang-en.json @@ -68,6 +68,7 @@ "Editing {%path%}.": "Editing {{path}}.", "Enable NAT traversal": "Enable NAT traversal", "Enable Relaying": "Enable Relaying", + "Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.", "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.", "Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.", "Error": "Error", @@ -242,6 +243,7 @@ "This Device": "This Device", "This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.", "This is a major version upgrade.": "This is a major version upgrade.", + "This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.", "Time": "Time", "Trash Can File Versioning": "Trash Can File Versioning", "Type": "Type", diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index a418e2d0c..8093252e0 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -62,7 +62,7 @@ angular.module('syncthing.core') selectedDevices: {}, type: "readwrite", rescanIntervalS: 60, - minDiskFreePct: 1, + minDiskFree: {value: 1, unit: "%"}, maxConflicts: 10, fsync: true, order: "random", diff --git a/gui/default/syncthing/folder/editFolderModalView.html b/gui/default/syncthing/folder/editFolderModalView.html index 359f90e17..63f1fd069 100644 --- a/gui/default/syncthing/folder/editFolderModalView.html +++ b/gui/default/syncthing/folder/editFolderModalView.html @@ -65,19 +65,28 @@
-
+
- +

The rescan interval must be a non-negative number of seconds.

-
- - -

- The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive). +

+
+
+
+
+
+

+ Enter a non-negative number (e.g., "2.35") and select a unit. Percentages are as part of the total disk size.

diff --git a/gui/default/syncthing/settings/settingsModalView.html b/gui/default/syncthing/settings/settingsModalView.html index d5842a15b..88f5b5670 100644 --- a/gui/default/syncthing/settings/settingsModalView.html +++ b/gui/default/syncthing/settings/settingsModalView.html @@ -78,6 +78,25 @@
+ +
+
+
+
+
+

+ Enter a non-negative number (e.g., "2.35") and select a unit. Percentages are as part of the total disk size. + This setting controls the free space required on the home (i.e., index database) disk. +

+
+
+
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"))