Files
filefast/backend/internal/service/device_service.go
2026-03-28 19:44:00 +08:00

214 lines
5.3 KiB
Go

package service
import (
"context"
"sort"
"strings"
"time"
"filefast/backend/internal/model"
"filefast/backend/internal/store"
"github.com/google/uuid"
)
type DeviceService struct {
store *store.MemoryStore
presenceStore devicePresenceStore
sessionStore deviceSessionStore
presenceTTL time.Duration
sessionTTL time.Duration
}
type devicePresenceStore interface {
SetDevicePresence(context.Context, string, bool, time.Time, time.Duration) error
GetDevicePresence(context.Context, []string) (map[string]bool, error)
}
type deviceSessionStore interface {
SaveDeviceSession(context.Context, model.DeviceSession, time.Duration) error
ValidateDeviceSession(context.Context, string, string) (bool, error)
}
type RegisterDeviceInput struct {
DeviceID string `json:"device_id"`
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
NetworkGroupKey string `json:"network_group_key"`
PublicIPHash string `json:"public_ip_hash"`
}
func NewDeviceService(store *store.MemoryStore, presenceStore devicePresenceStore, sessionStore deviceSessionStore) *DeviceService {
return &DeviceService{
store: store,
presenceStore: presenceStore,
sessionStore: sessionStore,
presenceTTL: 45 * time.Second,
sessionTTL: 30 * 24 * time.Hour,
}
}
func (s *DeviceService) Register(input RegisterDeviceInput, userAgent, claimedToken string) (model.Device, model.DeviceSession) {
now := time.Now()
id := strings.TrimSpace(input.DeviceID)
if id == "" {
id = uuid.NewString()
}
device, exists := s.store.GetDevice(id)
if exists && !s.ValidateSession(id, strings.TrimSpace(claimedToken)) {
id = uuid.NewString()
exists = false
}
if !exists {
device = model.Device{
ID: id,
CreatedAt: now,
}
}
device.Name = input.Name
device.Type = input.Type
device.UserAgent = userAgent
device.NetworkGroupKey = input.NetworkGroupKey
device.PublicIPHash = input.PublicIPHash
device.LastSeenAt = now
device.IsOnline = true
device = s.store.UpsertDevice(device)
session := s.issueSession(device.ID)
s.syncPresence(device)
return device, session
}
func (s *DeviceService) Heartbeat(deviceID string) (model.Device, bool) {
device, ok := s.store.GetDevice(deviceID)
if !ok {
return model.Device{}, false
}
device.LastSeenAt = time.Now()
device.IsOnline = true
device = s.store.UpsertDevice(device)
s.syncPresence(device)
return device, true
}
func (s *DeviceService) SetOnline(deviceID string, online bool) (model.Device, bool) {
device, ok := s.store.GetDevice(deviceID)
if !ok {
return model.Device{}, false
}
device.IsOnline = online
device.LastSeenAt = time.Now()
device = s.store.UpsertDevice(device)
s.syncPresence(device)
return device, true
}
func (s *DeviceService) ListCandidates(currentDeviceID string) []model.Device {
current, _ := s.store.GetDevice(currentDeviceID)
currentNetworkGroupKey := strings.TrimSpace(current.NetworkGroupKey)
if currentNetworkGroupKey == "" {
return nil
}
devices := s.store.ListDevices()
s.applyPresence(devices)
candidates := make([]model.Device, 0, len(devices))
for _, device := range devices {
if device.ID == currentDeviceID || !device.IsOnline {
continue
}
if strings.TrimSpace(device.NetworkGroupKey) != currentNetworkGroupKey {
continue
}
candidates = append(candidates, device)
}
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].LastSeenAt.After(candidates[j].LastSeenAt)
})
return candidates
}
func (s *DeviceService) ValidateSession(deviceID, token string) bool {
deviceID = strings.TrimSpace(deviceID)
token = strings.TrimSpace(token)
if deviceID == "" || token == "" {
return false
}
if s.store.ValidateDeviceSession(deviceID, token) {
return true
}
if s.sessionStore == nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ok, err := s.sessionStore.ValidateDeviceSession(ctx, deviceID, token)
return err == nil && ok
}
func (s *DeviceService) issueSession(deviceID string) model.DeviceSession {
session := model.DeviceSession{
DeviceID: deviceID,
Token: uuid.NewString(),
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(s.sessionTTL),
}
session = s.store.SaveDeviceSession(session)
if s.sessionStore != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = s.sessionStore.SaveDeviceSession(ctx, session, s.sessionTTL)
}
return session
}
func (s *DeviceService) syncPresence(device model.Device) {
if s.presenceStore == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = s.presenceStore.SetDevicePresence(ctx, device.ID, device.IsOnline, device.LastSeenAt, s.presenceTTL)
}
func (s *DeviceService) applyPresence(devices []model.Device) {
if s.presenceStore == nil || len(devices) == 0 {
return
}
deviceIDs := make([]string, 0, len(devices))
for _, device := range devices {
if device.ID != "" {
deviceIDs = append(deviceIDs, device.ID)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
statuses, err := s.presenceStore.GetDevicePresence(ctx, deviceIDs)
if err != nil {
return
}
for index := range devices {
if online, ok := statuses[devices[index].ID]; ok {
devices[index].IsOnline = online
}
}
}