Files
tyto/backend/internal/collectors/disk.go
vikingowl a2504c1327 feat: rename project to Tyto with owl branding
- Rename project from system-monitor to Tyto (barn owl themed)
- Update Go module name and all import paths
- Update Docker container names (tyto-backend, tyto-frontend)
- Update localStorage keys (tyto-settings, tyto-hosts)
- Create barn owl SVG favicon and PWA icons (192, 512)
- Update header with owl logo icon
- Update manifest.json and app.html with Tyto branding

Named after Tyto alba, the barn owl — nature's silent, watchful guardian

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 06:36:01 +01:00

168 lines
3.4 KiB
Go

package collectors
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"tyto/internal/models"
)
type DiskCollector struct {
procPath string
mtabPath string
}
func NewDiskCollector(procPath, mtabPath string) *DiskCollector {
return &DiskCollector{
procPath: procPath,
mtabPath: mtabPath,
}
}
func (c *DiskCollector) Collect() (models.DiskStats, error) {
stats := models.DiskStats{
Mounts: []models.MountStats{},
IO: []models.DiskIO{},
}
// Get mount points
mounts, err := c.getMounts()
if err == nil && mounts != nil {
stats.Mounts = mounts
}
// Get I/O stats
io, err := c.getIOStats()
if err == nil && io != nil {
stats.IO = io
}
return stats, nil
}
func (c *DiskCollector) getMounts() ([]models.MountStats, error) {
file, err := os.Open(c.mtabPath)
if err != nil {
// Fallback to /proc/mounts
file, err = os.Open(filepath.Join(c.procPath, "mounts"))
if err != nil {
return nil, err
}
}
defer file.Close()
var mounts []models.MountStats
scanner := bufio.NewScanner(file)
seen := make(map[string]bool)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 3 {
continue
}
device := fields[0]
mountPoint := fields[1]
fsType := fields[2]
// Skip virtual filesystems and duplicates
if !strings.HasPrefix(device, "/dev/") {
continue
}
if seen[mountPoint] {
continue
}
seen[mountPoint] = true
// Skip certain filesystem types
if fsType == "squashfs" || fsType == "overlay" {
continue
}
var stat syscall.Statfs_t
if err := syscall.Statfs(mountPoint, &stat); err != nil {
continue
}
total := stat.Blocks * uint64(stat.Bsize)
free := stat.Bfree * uint64(stat.Bsize)
available := stat.Bavail * uint64(stat.Bsize)
used := total - free
usedPercent := float64(0)
if total > 0 {
usedPercent = float64(used) / float64(total) * 100
}
mounts = append(mounts, models.MountStats{
Device: device,
MountPoint: mountPoint,
Filesystem: fsType,
Total: total,
Used: used,
Available: available,
UsedPercent: usedPercent,
})
}
return mounts, nil
}
func (c *DiskCollector) getIOStats() ([]models.DiskIO, error) {
file, err := os.Open(filepath.Join(c.procPath, "diskstats"))
if err != nil {
return nil, err
}
defer file.Close()
var stats []models.DiskIO
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 14 {
continue
}
device := fields[2]
// Only include real block devices (sd*, nvme*, vd*)
if !strings.HasPrefix(device, "sd") &&
!strings.HasPrefix(device, "nvme") &&
!strings.HasPrefix(device, "vd") {
continue
}
// Skip partitions (e.g., sda1, nvme0n1p1)
if strings.ContainsAny(device[len(device)-1:], "0123456789") {
lastChar := device[len(device)-1]
if lastChar >= '0' && lastChar <= '9' {
// Check if it's a partition
if strings.Contains(device, "nvme") {
if strings.Contains(device, "p") {
continue
}
} else {
continue
}
}
}
// Sectors read/written (field 5 and 9), sector = 512 bytes
readSectors, _ := strconv.ParseUint(fields[5], 10, 64)
writeSectors, _ := strconv.ParseUint(fields[9], 10, 64)
stats = append(stats, models.DiskIO{
Device: device,
ReadBytes: readSectors * 512,
WriteBytes: writeSectors * 512,
})
}
return stats, nil
}