Stop folder when running out of disk space (fixes #2057)

& tweaks by calmh
This commit is contained in:
Lode Hoste 2015-07-16 12:52:36 +02:00 committed by Jakob Borg
parent 6a58033f2b
commit dfaa999291
17 changed files with 217 additions and 1 deletions

4
Godeps/Godeps.json generated
View File

@ -9,6 +9,10 @@
"ImportPath": "github.com/bkaradzic/go-lz4", "ImportPath": "github.com/bkaradzic/go-lz4",
"Rev": "4f7c2045dbd17b802370e2e6022200468abf02ba" "Rev": "4f7c2045dbd17b802370e2e6022200468abf02ba"
}, },
{
"ImportPath": "github.com/calmh/du",
"Rev": "3c0690cca16228b97741327b1b6781397afbdb24"
},
{ {
"ImportPath": "github.com/calmh/logger", "ImportPath": "github.com/calmh/logger",
"Rev": "c96f6a1a8c7b6bf2f4860c667867d90174799eb2" "Rev": "c96f6a1a8c7b6bf2f4860c667867d90174799eb2"

24
Godeps/_workspace/src/github.com/calmh/du/LICENSE generated vendored Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

14
Godeps/_workspace/src/github.com/calmh/du/README.md generated vendored Normal file
View File

@ -0,0 +1,14 @@
du
==
Get total and available disk space on a given volume.
Documentation
-------------
http://godoc.org/github.com/calmh/du
License
-------
Public Domain

View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"log"
"os"
"github.com/calmh/du"
)
var KB = int64(1024)
func main() {
usage, err := du.Get(os.Args[1])
if err != nil {
log.Fatal(err)
}
fmt.Println("Free:", usage.FreeBytes/(KB*KB), "MiB")
fmt.Println("Available:", usage.AvailBytes/(KB*KB), "MiB")
fmt.Println("Size:", usage.TotalBytes/(KB*KB), "MiB")
}

View File

@ -0,0 +1,8 @@
package du
// Usage holds information about total and available storage on a volume.
type Usage struct {
TotalBytes int64 // Size of volume
FreeBytes int64 // Unused size
AvailBytes int64 // Available to a non-privileged user
}

View File

@ -0,0 +1,24 @@
// +build !windows,!netbsd,!openbsd,!solaris
package du
import (
"path/filepath"
"syscall"
)
// Get returns the Usage of a given path, or an error if usage data is
// unavailable.
func Get(path string) (Usage, error) {
var stat syscall.Statfs_t
err := syscall.Statfs(filepath.Clean(path), &stat)
if err != nil {
return Usage{}, err
}
u := Usage{
FreeBytes: int64(stat.Bfree) * int64(stat.Bsize),
TotalBytes: int64(stat.Blocks) * int64(stat.Bsize),
AvailBytes: int64(stat.Bavail) * int64(stat.Bsize),
}
return u, nil
}

View File

@ -0,0 +1,13 @@
// +build netbsd openbsd solaris
package du
import "errors"
var ErrUnsupported = errors.New("unsupported platform")
// Get returns the Usage of a given path, or an error if usage data is
// unavailable.
func Get(path string) (Usage, error) {
return Usage{}, ErrUnsupported
}

View File

@ -0,0 +1,27 @@
package du
import (
"syscall"
"unsafe"
)
// Get returns the Usage of a given path, or an error if usage data is
// unavailable.
func Get(path string) (Usage, error) {
h := syscall.MustLoadDLL("kernel32.dll")
c := h.MustFindProc("GetDiskFreeSpaceExW")
var u Usage
ret, _, err := c.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))),
uintptr(unsafe.Pointer(&u.FreeBytes)),
uintptr(unsafe.Pointer(&u.TotalBytes)),
uintptr(unsafe.Pointer(&u.AvailBytes)))
if ret == 0 {
return u, err
}
return u, nil
}

View File

@ -837,6 +837,7 @@ func defaultConfig(myName string) config.Configuration {
ID: "default", ID: "default",
RawPath: locations[locDefFolder], RawPath: locations[locDefFolder],
RescanIntervalS: 60, RescanIntervalS: 60,
MinDiskFreePct: 1,
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}}, Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
}, },
} }

13
internal/config/testdata/v11.xml vendored Normal file
View File

@ -0,0 +1,13 @@
<configuration version="11">
<folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>b</address>
</device>
</configuration>

View File

@ -26,7 +26,7 @@ import (
const ( const (
OldestHandledVersion = 5 OldestHandledVersion = 5
CurrentVersion = 10 CurrentVersion = 11
MaxRescanIntervalS = 365 * 24 * 60 * 60 MaxRescanIntervalS = 365 * 24 * 60 * 60
) )
@ -74,6 +74,7 @@ type FolderConfiguration struct {
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"` RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"` IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"` AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
MinDiskFreePct int `xml:"minDiskFreePct" json:"minDiskFreePct"`
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"` Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently. 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. 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 { if cfg.Version == 9 {
convertV9V10(cfg) convertV9V10(cfg)
} }
if cfg.Version == 10 {
convertV10V11(cfg)
}
// Hash old cleartext passwords // Hash old cleartext passwords
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' { if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
@ -460,6 +464,14 @@ func ChangeRequiresRestart(from, to Configuration) bool {
return false 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) { func convertV9V10(cfg *Configuration) {
// Enable auto normalization on existing folders. // Enable auto normalization on existing folders.
for i := range cfg.Folders { for i := range cfg.Folders {

View File

@ -92,6 +92,7 @@ func TestDeviceConfig(t *testing.T) {
Pullers: 16, Pullers: 16,
Hashers: 0, Hashers: 0,
AutoNormalize: true, AutoNormalize: true,
MinDiskFreePct: 1,
}, },
} }
expectedDevices := []DeviceConfiguration{ expectedDevices := []DeviceConfiguration{

View File

@ -96,6 +96,10 @@ func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
return Wrap(path, cfg), nil 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 stops the Serve() loop. Set and Replace operations will panic after a
// Stop. // Stop.
func (w *Wrapper) Stop() { func (w *Wrapper) Stop() {

View File

@ -45,6 +45,7 @@ const (
indexBatchSize = 1000 // Either way, don't include more files than this indexBatchSize = 1000 // Either way, don't include more files than this
reqValidationTime = time.Hour // How long to cache validation entries for Request messages 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 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 { type service interface {
@ -1230,6 +1231,10 @@ func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
return errors.New("no such folder") 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 _ = 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 // 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 // CheckFolderHealth checks the folder for common errors and returns the
// current folder error, or nil if the folder is healthy. // current folder error, or nil if the folder is healthy.
func (m *Model) CheckFolderHealth(id string) error { 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] folder, ok := m.cfg.Folders()[id]
if !ok { if !ok {
return errors.New("folder does not exist") return errors.New("folder does not exist")
@ -1673,6 +1682,8 @@ func (m *Model) CheckFolderHealth(id string) error {
err = errors.New("folder path missing") err = errors.New("folder path missing")
} else if !folder.HasMarker() { } else if !folder.HasMarker() {
err = errors.New("folder marker missing") 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) { } else if os.IsNotExist(err) {
// If we don't have any files in the index, and the directory // If we don't have any files in the index, and the directory

View File

@ -437,6 +437,7 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
// !!! // !!!
changed := 0 changed := 0
pullFileSize := int64(0)
fileDeletions := map[string]protocol.FileInfo{} fileDeletions := map[string]protocol.FileInfo{}
dirDeletions := []protocol.FileInfo{} dirDeletions := []protocol.FileInfo{}
@ -485,6 +486,7 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
default: default:
// A new or changed file or symlink. This is the only case where we // A new or changed file or symlink. This is the only case where we
// do stuff concurrently in the background // do stuff concurrently in the background
pullFileSize += file.Size()
p.queue.Push(file.Name, file.Size(), file.Modified) p.queue.Push(file.Name, file.Size(), file.Modified)
} }
@ -492,6 +494,17 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
return true 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 // Reorder the file queue according to configuration
switch p.order { switch p.order {

View File

@ -16,6 +16,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/calmh/du"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
) )
@ -210,3 +211,13 @@ func init() {
func IsWindowsExecutable(path string) bool { func IsWindowsExecutable(path string) bool {
return execExts[strings.ToLower(filepath.Ext(path))] 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
}

View File

@ -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)
}
}