279 lines
7.5 KiB
Go
279 lines
7.5 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"3588AdminBackend/internal/models"
|
|
"3588AdminBackend/internal/storage"
|
|
)
|
|
|
|
type ResourceManagementService struct {
|
|
repo *storage.ResourcesRepo
|
|
}
|
|
|
|
type InstalledResourceStatus struct {
|
|
Name string `json:"name"`
|
|
ResourceType string `json:"resource_type"`
|
|
SHA256 string `json:"sha256"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
type ResourceStatusCell struct {
|
|
ResourceName string `json:"resource_name"`
|
|
ResourceType string `json:"resource_type"`
|
|
Status string `json:"status"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
type ResourceStatusRow struct {
|
|
DeviceID string `json:"device_id"`
|
|
DeviceName string `json:"device_name"`
|
|
Online bool `json:"online"`
|
|
Cells []ResourceStatusCell `json:"cells"`
|
|
ExtraCount int `json:"extra_resource_count"`
|
|
ExtraResources []InstalledResourceStatus `json:"extra_resources"`
|
|
}
|
|
|
|
type ResourceStatusSummary struct {
|
|
StandardResources int `json:"standard_resources"`
|
|
Devices int `json:"devices"`
|
|
CompleteDevices int `json:"complete_devices"`
|
|
MissingDevices int `json:"missing_devices"`
|
|
MismatchDevices int `json:"mismatch_devices"`
|
|
}
|
|
|
|
type ResourceStatusBoard struct {
|
|
Summary ResourceStatusSummary `json:"summary"`
|
|
Rows []ResourceStatusRow `json:"rows"`
|
|
}
|
|
|
|
func NewResourceManagementService(repo *storage.ResourcesRepo) *ResourceManagementService {
|
|
return &ResourceManagementService{repo: repo}
|
|
}
|
|
|
|
func (s *ResourceManagementService) SyncStandardResourcesFromDirectory(dir string) error {
|
|
if s == nil || s.repo == nil {
|
|
return fmt.Errorf("resources repo is not configured")
|
|
}
|
|
dir = filepath.Clean(strings.TrimSpace(dir))
|
|
if dir == "" {
|
|
return fmt.Errorf("standard resources dir is empty")
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
// Subdirectory = resource_type; scan files inside
|
|
resourceType := entry.Name()
|
|
subDir := filepath.Join(dir, resourceType)
|
|
subEntries, serr := os.ReadDir(subDir)
|
|
if serr != nil {
|
|
continue
|
|
}
|
|
for _, sub := range subEntries {
|
|
if sub.IsDir() {
|
|
continue
|
|
}
|
|
fullPath := filepath.Join(subDir, sub.Name())
|
|
sum, size, err := hashFile(fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
record := storage.StandardResourceRecord{
|
|
Name: strings.TrimSuffix(sub.Name(), filepath.Ext(sub.Name())),
|
|
ResourceType: resourceType,
|
|
Version: "auto",
|
|
SHA256: sum,
|
|
SizeBytes: size,
|
|
FilePath: fullPath,
|
|
}
|
|
if err := s.repo.Save(record); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
// Flat file: infer resource_type from filename or leave empty (legacy)
|
|
fullPath := filepath.Join(dir, entry.Name())
|
|
sum, size, err := hashFile(fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
record := storage.StandardResourceRecord{
|
|
Name: strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())),
|
|
Version: "auto",
|
|
SHA256: sum,
|
|
SizeBytes: size,
|
|
FilePath: fullPath,
|
|
}
|
|
if err := s.repo.Save(record); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func FetchInstalledResourceStatuses(agent *AgentClient, dev *models.Device) ([]InstalledResourceStatus, error) {
|
|
if agent == nil || dev == nil || strings.TrimSpace(dev.IP) == "" || dev.AgentPort <= 0 {
|
|
return nil, nil
|
|
}
|
|
body, status, err := agent.Do("GET", dev.IP, dev.AgentPort, "/v1/resources/status", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status != 200 {
|
|
return nil, nil // agent may not implement this endpoint yet — return empty
|
|
}
|
|
var resp struct {
|
|
Resources []InstalledResourceStatus `json:"resources"`
|
|
}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Resources, nil
|
|
}
|
|
|
|
func BuildResourceStatusBoard(standardResources []storage.StandardResourceRecord, devices []*models.Device, installed map[string][]InstalledResourceStatus) ResourceStatusBoard {
|
|
board := ResourceStatusBoard{
|
|
Summary: ResourceStatusSummary{
|
|
StandardResources: len(standardResources),
|
|
Devices: len(devices),
|
|
},
|
|
Rows: make([]ResourceStatusRow, 0, len(devices)),
|
|
}
|
|
for _, device := range devices {
|
|
if device == nil {
|
|
continue
|
|
}
|
|
index := make(map[string]InstalledResourceStatus, len(installed[device.DeviceID]))
|
|
for _, item := range installed[device.DeviceID] {
|
|
index[item.Name] = item
|
|
}
|
|
standardIndex := make(map[string]struct{}, len(standardResources))
|
|
for _, res := range standardResources {
|
|
standardIndex[res.Name] = struct{}{}
|
|
}
|
|
row := ResourceStatusRow{
|
|
DeviceID: device.DeviceID,
|
|
DeviceName: device.DisplayName(),
|
|
Online: device.Online,
|
|
Cells: make([]ResourceStatusCell, 0, len(standardResources)),
|
|
ExtraResources: make([]InstalledResourceStatus, 0),
|
|
}
|
|
hasMissing := false
|
|
hasMismatch := false
|
|
for _, res := range standardResources {
|
|
cell := ResourceStatusCell{
|
|
ResourceName: res.Name,
|
|
ResourceType: res.ResourceType,
|
|
Version: res.Version,
|
|
Status: "missing",
|
|
}
|
|
if item, ok := index[res.Name]; ok {
|
|
if strings.EqualFold(strings.TrimSpace(item.SHA256), strings.TrimSpace(res.SHA256)) {
|
|
cell.Status = "ok"
|
|
} else {
|
|
cell.Status = "mismatch"
|
|
hasMismatch = true
|
|
}
|
|
} else {
|
|
hasMissing = true
|
|
}
|
|
if cell.Status == "missing" {
|
|
hasMissing = true
|
|
}
|
|
row.Cells = append(row.Cells, cell)
|
|
}
|
|
for _, item := range installed[device.DeviceID] {
|
|
if _, ok := standardIndex[item.Name]; ok {
|
|
continue
|
|
}
|
|
row.ExtraResources = append(row.ExtraResources, item)
|
|
}
|
|
sort.Slice(row.ExtraResources, func(i, j int) bool {
|
|
return row.ExtraResources[i].Name < row.ExtraResources[j].Name
|
|
})
|
|
row.ExtraCount = len(row.ExtraResources)
|
|
switch {
|
|
case hasMismatch:
|
|
board.Summary.MismatchDevices++
|
|
case hasMissing:
|
|
board.Summary.MissingDevices++
|
|
default:
|
|
board.Summary.CompleteDevices++
|
|
}
|
|
board.Rows = append(board.Rows, row)
|
|
}
|
|
sort.SliceStable(board.Rows, func(i, j int) bool {
|
|
return board.Rows[i].DeviceName < board.Rows[j].DeviceName
|
|
})
|
|
return board
|
|
}
|
|
|
|
// FetchAndBuildResourceBoard 并发查询所有在线设备的资源状态并构建面板,总超时 5 秒。
|
|
func FetchAndBuildResourceBoard(agent *AgentClient, devices []*models.Device, standardResources []storage.StandardResourceRecord) ResourceStatusBoard {
|
|
board := ResourceStatusBoard{
|
|
Summary: ResourceStatusSummary{
|
|
StandardResources: len(standardResources),
|
|
Devices: len(devices),
|
|
},
|
|
Rows: make([]ResourceStatusRow, 0, len(devices)),
|
|
}
|
|
if agent == nil || len(standardResources) == 0 {
|
|
return board
|
|
}
|
|
|
|
installed := make(map[string][]InstalledResourceStatus)
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
|
|
// 总超时:最多等 5 秒
|
|
done := make(chan struct{})
|
|
go func() {
|
|
time.Sleep(5 * time.Second)
|
|
close(done)
|
|
}()
|
|
|
|
for _, device := range devices {
|
|
if device == nil || !device.Online {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func(dev *models.Device) {
|
|
defer wg.Done()
|
|
items, err := FetchInstalledResourceStatuses(agent, dev)
|
|
if err == nil {
|
|
mu.Lock()
|
|
installed[dev.DeviceID] = items
|
|
mu.Unlock()
|
|
}
|
|
}(device)
|
|
}
|
|
|
|
// Wait with timeout
|
|
waitCh := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(waitCh)
|
|
}()
|
|
select {
|
|
case <-waitCh:
|
|
case <-done:
|
|
}
|
|
|
|
return BuildResourceStatusBoard(standardResources, devices, installed)
|
|
}
|