mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 10:58:57 +00:00
Stop folder when running out of disk space (fixes #2057)
& tweaks by calmh
This commit is contained in:
parent
6a58033f2b
commit
dfaa999291
4
Godeps/Godeps.json
generated
4
Godeps/Godeps.json
generated
@ -9,6 +9,10 @@
|
||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||
"Rev": "4f7c2045dbd17b802370e2e6022200468abf02ba"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/du",
|
||||
"Rev": "3c0690cca16228b97741327b1b6781397afbdb24"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/logger",
|
||||
"Rev": "c96f6a1a8c7b6bf2f4860c667867d90174799eb2"
|
||||
|
24
Godeps/_workspace/src/github.com/calmh/du/LICENSE
generated
vendored
Normal file
24
Godeps/_workspace/src/github.com/calmh/du/LICENSE
generated
vendored
Normal 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
14
Godeps/_workspace/src/github.com/calmh/du/README.md
generated
vendored
Normal 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
|
21
Godeps/_workspace/src/github.com/calmh/du/cmd/du/main.go
generated
vendored
Normal file
21
Godeps/_workspace/src/github.com/calmh/du/cmd/du/main.go
generated
vendored
Normal 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")
|
||||
}
|
8
Godeps/_workspace/src/github.com/calmh/du/diskusage.go
generated
vendored
Normal file
8
Godeps/_workspace/src/github.com/calmh/du/diskusage.go
generated
vendored
Normal 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
|
||||
}
|
24
Godeps/_workspace/src/github.com/calmh/du/diskusage_posix.go
generated
vendored
Normal file
24
Godeps/_workspace/src/github.com/calmh/du/diskusage_posix.go
generated
vendored
Normal 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
|
||||
}
|
13
Godeps/_workspace/src/github.com/calmh/du/diskusage_unsupported.go
generated
vendored
Normal file
13
Godeps/_workspace/src/github.com/calmh/du/diskusage_unsupported.go
generated
vendored
Normal 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
|
||||
}
|
27
Godeps/_workspace/src/github.com/calmh/du/diskusage_windows.go
generated
vendored
Normal file
27
Godeps/_workspace/src/github.com/calmh/du/diskusage_windows.go
generated
vendored
Normal 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
|
||||
}
|
@ -837,6 +837,7 @@ func defaultConfig(myName string) config.Configuration {
|
||||
ID: "default",
|
||||
RawPath: locations[locDefFolder],
|
||||
RescanIntervalS: 60,
|
||||
MinDiskFreePct: 1,
|
||||
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
|
||||
},
|
||||
}
|
||||
|
13
internal/config/testdata/v11.xml
vendored
Normal file
13
internal/config/testdata/v11.xml
vendored
Normal 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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user