133 lines
4.2 KiB
Go
133 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// storedOn are the filesystem types that hold data on a device or a server. Everything else in the
|
|
// mount table (proc, sysfs, cgroup, tmpfs, overlay layers, squashfs images…) is the kernel's or a copy.
|
|
var storedOn = map[string]bool{
|
|
"ext2": true, "ext3": true, "ext4": true, "xfs": true, "btrfs": true, "zfs": true, "f2fs": true,
|
|
"bcachefs": true, "vfat": true, "exfat": true, "ntfs": true, "ntfs3": true, "fuseblk": true,
|
|
"nfs": true, "nfs4": true, "cifs": true, "smb3": true,
|
|
}
|
|
|
|
// mount is one line of the mount table.
|
|
type mount struct{ source, target, fstype string }
|
|
|
|
// unescape undoes the mount table's octal escapes (a space is \040).
|
|
func unescape(s string) string {
|
|
if !strings.Contains(s, `\`) {
|
|
return s
|
|
}
|
|
var b strings.Builder
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\\' && i+3 < len(s) {
|
|
o := s[i+1 : i+4]
|
|
if len(o) == 3 && o[0] >= '0' && o[0] <= '3' && o[1] >= '0' && o[1] <= '7' && o[2] >= '0' && o[2] <= '7' {
|
|
b.WriteByte((o[0]-'0')<<6 | (o[1]-'0')<<3 | (o[2] - '0'))
|
|
i += 3
|
|
continue
|
|
}
|
|
}
|
|
b.WriteByte(s[i])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// mounts is this process's mount table, only the filesystems that store data.
|
|
func (m *Machine) mounts() []mount {
|
|
var out []mount
|
|
for _, line := range strings.Split(m.read("/proc/self/mounts"), "\n") {
|
|
f := strings.Fields(line)
|
|
if len(f) < 3 || !storedOn[f[2]] {
|
|
continue
|
|
}
|
|
out = append(out, mount{source: unescape(f[0]), target: unescape(f[1]), fstype: f[2]})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Filesystem is one filesystem's space, once however many places it is mounted at.
|
|
type Filesystem struct {
|
|
Mount string `json:"mount"`
|
|
AlsoAt []string `json:"also_at,omitempty"`
|
|
Source string `json:"source"`
|
|
Device string `json:"device,omitempty"`
|
|
Type string `json:"type"`
|
|
SizeGiB float64 `json:"size_gib"`
|
|
UsedGiB float64 `json:"used_gib"`
|
|
AvailGiB float64 `json:"available_gib"`
|
|
UsedPct float64 `json:"used_pct"`
|
|
InodesUsed float64 `json:"inodes_used_pct,omitempty"`
|
|
Unanswered string `json:"error,omitempty"`
|
|
}
|
|
|
|
// blockName is the kernel's name for the device a mount's source names: /dev/nvme0n1p3 is nvme0n1p3,
|
|
// /dev/mapper/root the dm-N whose name is root; "" for a source that is no local device.
|
|
func (m *Machine) blockName(source string, mappers map[string]string) string {
|
|
if !strings.HasPrefix(source, "/dev/") {
|
|
return ""
|
|
}
|
|
if name, ok := strings.CutPrefix(source, "/dev/mapper/"); ok {
|
|
return mappers[name]
|
|
}
|
|
// /dev/disk/by-uuid/… and the like are links the system made; reading where one points creates nothing.
|
|
if m.Root == "/" {
|
|
if real, err := filepath.EvalSymlinks(source); err == nil {
|
|
source = real
|
|
}
|
|
}
|
|
return filepath.Base(source)
|
|
}
|
|
|
|
// mappers is each device-mapper name with the dm-N it is.
|
|
func (m *Machine) mappers() map[string]string {
|
|
out := map[string]string{}
|
|
for _, dev := range m.names("/sys/block") {
|
|
if name := m.mapperName(dev); name != "" {
|
|
out[name] = dev
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Filesystems is the space of every filesystem that stores data. A filesystem mounted at several
|
|
// places (a btrfs subvolume, a bind mount) is said once, at its first mount, with the others beside it.
|
|
func (m *Machine) Filesystems() []Filesystem {
|
|
mappers := m.mappers()
|
|
var out []Filesystem
|
|
seen := map[string]int{}
|
|
for _, mt := range m.mounts() {
|
|
key := mt.source
|
|
if i, ok := seen[key]; ok {
|
|
out[i].AlsoAt = append(out[i].AlsoAt, mt.target)
|
|
continue
|
|
}
|
|
fs := Filesystem{Mount: mt.target, Source: mt.source, Type: mt.fstype, Device: m.blockName(mt.source, mappers)}
|
|
st, err := m.Statfs(m.path(mt.target))
|
|
if err != nil {
|
|
fs.Unanswered = err.Error()
|
|
} else if st.Size > 0 {
|
|
used := st.Size - st.Free
|
|
fs.SizeGiB = round1(float64(st.Size) / gib)
|
|
fs.UsedGiB = round1(float64(used) / gib)
|
|
fs.AvailGiB = round1(float64(st.Avail) / gib)
|
|
// As df counts it: used against what an ordinary user could have, so the reserve shows as full.
|
|
if used+st.Avail > 0 {
|
|
fs.UsedPct = round1(float64(used) / float64(used+st.Avail) * 100)
|
|
}
|
|
if st.Files > 0 {
|
|
fs.InodesUsed = round1(float64(st.Files-st.FilesFree) / float64(st.Files) * 100)
|
|
}
|
|
}
|
|
seen[key] = len(out)
|
|
out = append(out, fs)
|
|
}
|
|
if out == nil {
|
|
out = []Filesystem{}
|
|
}
|
|
return out
|
|
}
|