94 lines
2.5 KiB
Go
94 lines
2.5 KiB
Go
package backup
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
)
|
|
|
|
// MinFreeAfter is the share of the archive filesystem an on-demand backup must
|
|
// leave free. The archive store sits on the node's disk beside the worlds and
|
|
// the database; below about a tenth free the kubelet starts evicting pods
|
|
// (docs/troubleshooting.md §13b), so a backup that would cross it is refused.
|
|
const MinFreeAfter = 0.10
|
|
|
|
// ErrNoRoom is returned by CheckRoom when the archive would leave too little
|
|
// free space.
|
|
var ErrNoRoom = errors.New("backup: not enough free disk for the archive")
|
|
|
|
// CheckRoom refuses an archive of srcDir into archiveDir that could push the
|
|
// archive filesystem below minFree free. The world's uncompressed size stands
|
|
// in for the archive's, which gzip only makes smaller. archiveDir need not
|
|
// exist yet; its nearest existing parent is measured.
|
|
func CheckRoom(archiveDir, srcDir string, minFree float64) error {
|
|
dir := archiveDir
|
|
for {
|
|
if _, err := os.Stat(dir); err == nil {
|
|
break
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return fmt.Errorf("backup: no existing directory above %s", archiveDir)
|
|
}
|
|
dir = parent
|
|
}
|
|
var st syscall.Statfs_t
|
|
if err := syscall.Statfs(dir, &st); err != nil {
|
|
return fmt.Errorf("backup: measure %s: %w", dir, err)
|
|
}
|
|
bsize := uint64(st.Bsize) // uint32 on darwin
|
|
total := uint64(st.Blocks) * bsize
|
|
avail := uint64(st.Bavail) * bsize
|
|
if total == 0 {
|
|
return nil
|
|
}
|
|
need, err := treeBytes(srcDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
floor := uint64(float64(total) * minFree)
|
|
if avail < need || avail-need < floor {
|
|
return fmt.Errorf("%w: the world is %s and %s is free of %s, which would leave less than %.0f%% free",
|
|
ErrNoRoom, byteSize(need), byteSize(avail), byteSize(total), minFree*100)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// treeBytes sums the sizes of the regular files under dir.
|
|
func treeBytes(dir string) (uint64, error) {
|
|
var n uint64
|
|
err := filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.Type().IsRegular() {
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n += uint64(info.Size())
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return 0, fmt.Errorf("backup: measure the world: %w", err)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func byteSize(b uint64) string {
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
div, exp := uint64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
}
|