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 }