package watcher import ( "context" "os" "path/filepath" "sort" "sync" "time" "jukebox_maker/internal/disk" ) type DiskEvent struct { Info disk.DiskInfo Prev disk.DiskInfo } type Handler func(event DiskEvent) type Watcher struct { mountPath string interval time.Duration handler Handler mu sync.RWMutex disks map[string]disk.DiskInfo } func New(mountPath string, interval time.Duration, handler Handler) *Watcher { return &Watcher{ mountPath: mountPath, interval: interval, handler: handler, disks: make(map[string]disk.DiskInfo), } } func (w *Watcher) ListDisks() []disk.DiskInfo { w.mu.RLock() defer w.mu.RUnlock() items := make([]disk.DiskInfo, 0, len(w.disks)) for _, info := range w.disks { items = append(items, info) } sort.Slice(items, func(i, j int) bool { return items[i].MountPath < items[j].MountPath }) return items } func (w *Watcher) DiskByMountPath(mountPath string) (disk.DiskInfo, bool) { w.mu.RLock() defer w.mu.RUnlock() info, ok := w.disks[mountPath] return info, ok } func (w *Watcher) DiskByID(diskID string) (disk.DiskInfo, bool) { w.mu.RLock() defer w.mu.RUnlock() for _, info := range w.disks { if info.DiskID == diskID { return info, true } } return disk.DiskInfo{}, false } func (w *Watcher) ProbeNow() { w.probe() } func (w *Watcher) Run(ctx context.Context) { ticker := time.NewTicker(w.interval) defer ticker.Stop() // probe immediately on start w.probe() for { select { case <-ctx.Done(): return case <-ticker.C: w.probe() } } } func (w *Watcher) probe() { next := discoverDisks(w.mountPath) w.mu.Lock() prev := w.disks w.disks = next w.mu.Unlock() if w.handler == nil { return } seen := make(map[string]struct{}, len(prev)+len(next)) for mountPath, info := range next { seen[mountPath] = struct{}{} prevInfo := prev[mountPath] if prevInfo.State != info.State || prevInfo.DiskID != info.DiskID { w.handler(DiskEvent{Info: info, Prev: prevInfo}) } } for mountPath, prevInfo := range prev { if _, ok := seen[mountPath]; ok { continue } w.handler(DiskEvent{ Info: disk.DiskInfo{ State: disk.DiskAbsent, MountPath: mountPath, }, Prev: prevInfo, }) } } func discoverDisks(root string) map[string]disk.DiskInfo { entries, err := os.ReadDir(root) if err != nil { return map[string]disk.DiskInfo{} } disks := make(map[string]disk.DiskInfo) for _, entry := range entries { if !entry.IsDir() { continue } mountPath := filepath.Join(root, entry.Name()) info, _ := disk.Probe(mountPath) if info.State == disk.DiskAbsent { continue } disks[mountPath] = info } return disks }