Stop folder when running out of disk space (fixes #2057)
& tweaks by calmh
This commit is contained in:
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
const (
|
||||
OldestHandledVersion = 5
|
||||
CurrentVersion = 10
|
||||
CurrentVersion = 11
|
||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
@@ -74,6 +74,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 int `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
||||
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.
|
||||
@@ -364,6 +365,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
if cfg.Version == 9 {
|
||||
convertV9V10(cfg)
|
||||
}
|
||||
if cfg.Version == 10 {
|
||||
convertV10V11(cfg)
|
||||
}
|
||||
|
||||
// Hash old cleartext passwords
|
||||
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
||||
@@ -460,6 +464,14 @@ func ChangeRequiresRestart(from, to Configuration) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func convertV10V11(cfg *Configuration) {
|
||||
// Set minimum disk free of existing folders to 1%
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].MinDiskFreePct = 1
|
||||
}
|
||||
cfg.Version = 11
|
||||
}
|
||||
|
||||
func convertV9V10(cfg *Configuration) {
|
||||
// Enable auto normalization on existing folders.
|
||||
for i := range cfg.Folders {
|
||||
|
||||
@@ -92,6 +92,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
Pullers: 16,
|
||||
Hashers: 0,
|
||||
AutoNormalize: true,
|
||||
MinDiskFreePct: 1,
|
||||
},
|
||||
}
|
||||
expectedDevices := []DeviceConfiguration{
|
||||
|
||||
@@ -96,6 +96,10 @@ func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
|
||||
return Wrap(path, cfg), nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) ConfigPath() string {
|
||||
return w.path
|
||||
}
|
||||
|
||||
// Stop stops the Serve() loop. Set and Replace operations will panic after a
|
||||
// Stop.
|
||||
func (w *Wrapper) Stop() {
|
||||
|
||||
@@ -45,6 +45,7 @@ const (
|
||||
indexBatchSize = 1000 // Either way, don't include more files than this
|
||||
reqValidationTime = time.Hour // How long to cache validation entries for Request messages
|
||||
reqValidationCacheSize = 1000 // How many entries to aim for in the validation cache size
|
||||
minHomeDiskFreePct = 1.0 // Stop when less space than this is available on the home (config & db) disk
|
||||
)
|
||||
|
||||
type service interface {
|
||||
@@ -1230,6 +1231,10 @@ func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
|
||||
return errors.New("no such folder")
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = ignores.Load(filepath.Join(folderCfg.Path(), ".stignore")) // Ignore error, there might not be an .stignore
|
||||
|
||||
// Required to make sure that we start indexing at a directory we're already
|
||||
@@ -1658,6 +1663,10 @@ func (m *Model) BringToFront(folder, file string) {
|
||||
// CheckFolderHealth checks the folder for common errors and returns the
|
||||
// current folder error, or nil if the folder is healthy.
|
||||
func (m *Model) CheckFolderHealth(id string) error {
|
||||
if free, err := osutil.DiskFreePercentage(m.cfg.ConfigPath()); err == nil && free < minHomeDiskFreePct {
|
||||
return errors.New("out of disk space")
|
||||
}
|
||||
|
||||
folder, ok := m.cfg.Folders()[id]
|
||||
if !ok {
|
||||
return errors.New("folder does not exist")
|
||||
@@ -1673,6 +1682,8 @@ func (m *Model) CheckFolderHealth(id string) error {
|
||||
err = errors.New("folder path missing")
|
||||
} else if !folder.HasMarker() {
|
||||
err = errors.New("folder marker missing")
|
||||
} else if free, errDfp := osutil.DiskFreePercentage(folder.Path()); errDfp == nil && free < float64(folder.MinDiskFreePct) {
|
||||
err = errors.New("out of disk space")
|
||||
}
|
||||
} else if os.IsNotExist(err) {
|
||||
// If we don't have any files in the index, and the directory
|
||||
|
||||
@@ -437,6 +437,7 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
// !!!
|
||||
|
||||
changed := 0
|
||||
pullFileSize := int64(0)
|
||||
|
||||
fileDeletions := map[string]protocol.FileInfo{}
|
||||
dirDeletions := []protocol.FileInfo{}
|
||||
@@ -485,6 +486,7 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
default:
|
||||
// A new or changed file or symlink. This is the only case where we
|
||||
// do stuff concurrently in the background
|
||||
pullFileSize += file.Size()
|
||||
p.queue.Push(file.Name, file.Size(), file.Modified)
|
||||
}
|
||||
|
||||
@@ -492,6 +494,17 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
return true
|
||||
})
|
||||
|
||||
// Check if we are able to store all files on disk
|
||||
if pullFileSize > 0 {
|
||||
folder, ok := p.model.cfg.Folders()[p.folder]
|
||||
if ok {
|
||||
if free, err := osutil.DiskFreeBytes(folder.Path()); err == nil && free < pullFileSize {
|
||||
l.Infof("Puller (folder %q): insufficient disk space available to pull %d files (%.2fMB)", p.folder, changed, float64(pullFileSize)/1024/1024)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder the file queue according to configuration
|
||||
|
||||
switch p.order {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/du"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
@@ -210,3 +211,13 @@ func init() {
|
||||
func IsWindowsExecutable(path string) bool {
|
||||
return execExts[strings.ToLower(filepath.Ext(path))]
|
||||
}
|
||||
|
||||
func DiskFreeBytes(path string) (free int64, err error) {
|
||||
u, err := du.Get(path)
|
||||
return u.FreeBytes, err
|
||||
}
|
||||
|
||||
func DiskFreePercentage(path string) (freePct float64, err error) {
|
||||
u, err := du.Get(path)
|
||||
return (float64(u.FreeBytes) / float64(u.TotalBytes)) * 100, err
|
||||
}
|
||||
|
||||
@@ -164,3 +164,18 @@ func TestInWritableDirWindowsRename(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskUsage(t *testing.T) {
|
||||
free, err := osutil.DiskFreePercentage(".")
|
||||
if err != nil {
|
||||
if runtime.GOOS == "netbsd" ||
|
||||
runtime.GOOS == "openbsd" ||
|
||||
runtime.GOOS == "solaris" {
|
||||
t.Skip()
|
||||
}
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if free < 1 {
|
||||
t.Error("Disk is full?", free)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user