Files
ProxmoxDash/backups.go
2026-07-31 21:12:22 +03:00

117 lines
2.9 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"os/exec"
"sort"
"strings"
"sync"
"time"
)
type BackupItem struct {
VMID int `json:"vmid"`
Storage string `json:"storage"`
VolumeID string `json:"volid"`
Format string `json:"format"`
SizeBytes uint64 `json:"size"`
CreatedAt int64 `json:"ctime"`
}
type BackupMetrics struct {
Available bool `json:"available"`
Items []BackupItem `json:"items"`
}
type backupCollector struct {
mu sync.Mutex
cached BackupMetrics
updatedAt time.Time
}
func (c *backupCollector) collect() BackupMetrics {
c.mu.Lock()
defer c.mu.Unlock()
if time.Since(c.updatedAt) < time.Minute {
return c.cached
}
c.cached = readBackups()
c.updatedAt = time.Now()
return c.cached
}
func readBackups() BackupMetrics {
if _, err := exec.LookPath("pvesh"); err != nil {
return BackupMetrics{}
}
node, err := os.Hostname()
if err != nil {
return BackupMetrics{Available: true}
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+node+"/storage", "--content", "backup", "--enabled", "1", "--output-format", "json").Output()
result := BackupMetrics{Available: true}
if err != nil {
return result
}
var storages []struct {
Storage string `json:"storage"`
Active int `json:"active"`
}
if json.Unmarshal(output, &storages) != nil {
return result
}
var items []BackupItem
for _, storage := range storages {
if storage.Storage == "" || storage.Active == 0 {
continue
}
content, err := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+node+"/storage/"+storage.Storage+"/content", "--content", "backup", "--output-format", "json").Output()
if err != nil {
continue
}
var storageItems []BackupItem
if json.Unmarshal(content, &storageItems) == nil {
for i := range storageItems {
storageItems[i].Storage = storage.Storage
if storageItems[i].Format == "" {
storageItems[i].Format = backupFormat(storageItems[i].VolumeID)
}
}
items = append(items, storageItems...)
}
}
result.Items = latestBackups(items)
return result
}
func latestBackups(items []BackupItem) []BackupItem {
latest := make(map[int]BackupItem)
for _, item := range items {
if item.VMID == 0 {
continue
}
if previous, ok := latest[item.VMID]; !ok || item.CreatedAt > previous.CreatedAt {
latest[item.VMID] = item
}
}
result := make([]BackupItem, 0, len(latest))
for _, item := range latest {
result = append(result, item)
}
sort.Slice(result, func(i, j int) bool { return result[i].VMID < result[j].VMID })
return result
}
func backupFormat(volumeID string) string {
for _, suffix := range []string{".vma.zst", ".vma.lzo", ".vma.gz", ".tar.zst", ".tar.lzo", ".tar.gz"} {
if strings.HasSuffix(volumeID, suffix) {
return strings.TrimPrefix(suffix, ".")
}
}
return "backup"
}