Add backup dashboard and appearance settings
This commit is contained in:
116
backups.go
Normal file
116
backups.go
Normal file
@@ -0,0 +1,116 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user