package unraid import ( "context" "encoding/json" "errors" "strings" "time" "github.com/itworx/pulse/internal/share" ) // ShareSource maps Unraid's bounded, read-only shares query. Share usage is reported // by Unraid in KiB; Pulse's domain contract consistently uses bytes. The API does not // expose a complete placement scan in this selection, so no placement is inferred. type ShareSource struct { Client *Client Now func() time.Time } func (s ShareSource) Snapshot(ctx context.Context) (share.RawSnapshot, error) { if s.Client == nil { return share.RawSnapshot{}, errors.New("Unraid client is required") } payload, err := s.Client.Query(ctx, "shares") if err != nil { return share.RawSnapshot{}, err } var response struct { Shares []struct { ID string `json:"id"` Name string `json:"name"` Used json.RawMessage `json:"used"` Size json.RawMessage `json:"size"` Include []string `json:"include"` Exclude []string `json:"exclude"` Cache bool `json:"cache"` Allocator string `json:"allocator"` } `json:"shares"` } if err := json.Unmarshal(payload, &response); err != nil { return share.RawSnapshot{}, errors.New("decode Unraid shares response") } if len(response.Shares) > 1000 { return share.RawSnapshot{}, errors.New("Unraid share response exceeds bounds") } now := time.Now().UTC() if s.Now != nil { now = s.Now().UTC() } items := make([]share.RawShare, 0, len(response.Shares)) for _, item := range response.Shares { used, err := rawUint(item.Used) if err != nil || strings.TrimSpace(item.ID) == "" || strings.TrimSpace(item.Name) == "" { return share.RawSnapshot{}, errors.New("Unraid share identity or usage is invalid") } // `cache` is a boolean in this API version. Preserve exactly that fact instead // of inventing a legacy mover policy or a primary/secondary placement. cachePolicy := "disabled" if item.Cache { cachePolicy = "enabled" } items = append(items, share.RawShare{ ID: item.ID, Name: item.Name, StoragePolicy: share.StoragePolicy{Allocation: strings.TrimSpace(item.Allocator), CachePolicy: cachePolicy}, UsedBytes: kilobytes(used), SizeObservedAt: now, SizeState: share.SizeAvailable, }) } return share.RawSnapshot{Source: share.Source{ID: "unraid", Type: "unraid"}, Shares: items, ObservedAt: now, ReceivedAt: now}, nil }