Merge branch 'pr-2066'

* pr-2066:
  Configurable home disk percentage, translations
  Add minimum disk free percentage to GUI
  Stop folder when running out of disk space (fixes #2057)
This commit is contained in:
Jakob Borg 2015-08-09 10:38:33 +02:00
commit 2bcb57c994
21 changed files with 235 additions and 5 deletions

4
Godeps/Godeps.json generated
View File

@ -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
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",
RawPath: locations[locDefFolder],
RescanIntervalS: 60,
MinDiskFreePct: 1,
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
},
}

View File

@ -93,6 +93,7 @@
"Major Upgrade": "Major Upgrade",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Move to top of queue": "Move to top of queue",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
@ -120,6 +121,7 @@
"RAM Utilization": "RAM Utilization",
"Random": "Random",
"Release Notes": "Release Notes",
"Remove": "Remove",
"Rescan": "Rescan",
"Rescan All": "Rescan All",
"Rescan Interval": "Rescan Interval",
@ -176,6 +178,7 @@
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.",

View File

@ -1057,6 +1057,7 @@ angular.module('syncthing.core')
selectedDevices: {}
};
$scope.currentFolder.rescanIntervalS = 60;
$scope.currentFolder.minDiskFreePct = 1;
$scope.currentFolder.order = "random";
$scope.currentFolder.fileVersioningSelector = "none";
$scope.currentFolder.trashcanClean = 0;
@ -1077,6 +1078,7 @@ angular.module('syncthing.core')
id: folder,
selectedDevices: {},
rescanIntervalS: 60,
minDiskFreePct: 1,
fileVersioningSelector: "none",
trashcanClean: 0,
simpleKeep: 5,

View File

@ -37,6 +37,13 @@
<span translate ng-if="!folderEditor.rescanIntervalS.$valid && folderEditor.rescanIntervalS.$dirty">The rescan interval must be a non-negative number of seconds.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.minDiskFreePct.$invalid && folderEditor.minDiskFreePct.$dirty}">
<label for="minDiskFreePct"><span translate>Minimum Free Disk Space</span> (0-100%)</label>
<input name="minDiskFreePct" id="minDiskFreePct" class="form-control" type="number" ng-model="currentFolder.minDiskFreePct" required min="0" max="100"></input>
<p class="help-block">
<span translate ng-if="!folderEditor.minDiskFreePct.$valid && folderEditor.minDiskFreePct.$dirty">The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).</span>
</p>
</div>
</div>
</div>
<div class="row">

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>

File diff suppressed because one or more lines are too long

View File

@ -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.
@ -237,6 +238,7 @@ type OptionsConfiguration struct {
DatabaseBlockCacheMiB int `xml:"databaseBlockCacheMiB" json:"databaseBlockCacheMiB" default:"0"`
PingTimeoutS int `xml:"pingTimeoutS" json:"pingTimeoutS" default:"30"`
PingIdleTimeS int `xml:"pingIdleTimeS" json:"pingIdleTimeS" default:"60"`
MinHomeDiskFreePct int `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
}
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
@ -364,6 +366,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 +465,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 {

View File

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

View File

@ -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() {

View File

@ -1230,6 +1230,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 +1662,12 @@ 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 minFree := float64(m.cfg.Options().MinHomeDiskFreePct); minFree > 0 {
if free, err := osutil.DiskFreePercentage(m.cfg.ConfigPath()); err == nil && free < minFree {
return errors.New("home disk is out of space")
}
}
folder, ok := m.cfg.Folders()[id]
if !ok {
return errors.New("folder does not exist")
@ -1673,6 +1683,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

View File

@ -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 {

View File

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

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