package cache import ( "context" "fmt" "time" ) // DownloadProgress represents real-time download progress type DownloadProgress struct { JobID string `json:"jobId"` Status string `json:"status"` ProgressPercent int `json:"progressPercent"` BytesTotal int64 `json:"bytesTotal"` BytesDownloaded int64 `json:"bytesDownloaded"` DownloadSpeedMbps float64 `json:"downloadSpeedMbps"` EtaSeconds int `json:"etaSeconds"` UpdatedAt time.Time `json:"updatedAt"` } // DownloadCache provides caching for download operations type DownloadCache struct { service *Service keys *KeyBuilder } // NewDownloadCache creates a new download cache func NewDownloadCache(service *Service, namespace string) *DownloadCache { return &DownloadCache{ service: service, keys: NewKeyBuilder(namespace), } } // SetProgress stores download progress in cache func (dc *DownloadCache) SetProgress(ctx context.Context, progress DownloadProgress) error { key := dc.keys.DownloadJobKey(progress.JobID) progress.UpdatedAt = time.Now().UTC() return dc.service.Set(ctx, key, progress, TTLDownload) } // GetProgress retrieves download progress from cache func (dc *DownloadCache) GetProgress(ctx context.Context, jobID string) (*DownloadProgress, error) { key := dc.keys.DownloadJobKey(jobID) var progress DownloadProgress if err := dc.service.Get(ctx, key, &progress); err != nil { return nil, err } return &progress, nil } // DeleteProgress removes download progress from cache func (dc *DownloadCache) DeleteProgress(ctx context.Context, jobID string) error { key := dc.keys.DownloadJobKey(jobID) return dc.service.Delete(ctx, key) } // GetUserDownloads retrieves cached download list for a user func (dc *DownloadCache) GetUserDownloads(ctx context.Context, userID, status string, target interface{}) error { key := dc.keys.DownloadListKey(userID, status) return dc.service.Get(ctx, key, target) } // SetUserDownloads stores download list in cache func (dc *DownloadCache) SetUserDownloads(ctx context.Context, userID, status string, data interface{}) error { key := dc.keys.DownloadListKey(userID, status) return dc.service.Set(ctx, key, data, TTLDownload) } // InvalidateUserDownloads removes cached download list func (dc *DownloadCache) InvalidateUserDownloads(ctx context.Context, userID string) error { // Invalidate all status variations statuses := []string{"", "queued", "preparing", "downloading", "completed", "failed", "cancelled"} keys := make([]string, 0, len(statuses)) for _, status := range statuses { keys = append(keys, dc.keys.DownloadListKey(userID, status)) } return dc.service.Delete(ctx, keys...) } // UpdateProgressField updates a specific field of download progress func (dc *DownloadCache) UpdateProgressField(ctx context.Context, jobID string, updateFunc func(*DownloadProgress)) error { progress, err := dc.GetProgress(ctx, jobID) if err != nil { if err == ErrCacheMiss { // Create new progress entry progress = &DownloadProgress{ JobID: jobID, UpdatedAt: time.Now().UTC(), } } else { return err } } updateFunc(progress) return dc.SetProgress(ctx, *progress) } // IncrementDownloadedBytes atomically increments downloaded bytes func (dc *DownloadCache) IncrementDownloadedBytes(ctx context.Context, jobID string, bytes int64) error { return dc.UpdateProgressField(ctx, jobID, func(p *DownloadProgress) { p.BytesDownloaded += bytes if p.BytesTotal > 0 { p.ProgressPercent = int((p.BytesDownloaded * 100) / p.BytesTotal) } }) } // SetDownloadSpeed updates the download speed func (dc *DownloadCache) SetDownloadSpeed(ctx context.Context, jobID string, speedMbps float64) error { return dc.UpdateProgressField(ctx, jobID, func(p *DownloadProgress) { p.DownloadSpeedMbps = speedMbps // Calculate ETA if we have speed and remaining bytes if speedMbps > 0 && p.BytesTotal > 0 { remainingBytes := p.BytesTotal - p.BytesDownloaded if remainingBytes > 0 { // Convert Mbps to bytes per second bytesPerSecond := (speedMbps * 1024 * 1024) / 8 p.EtaSeconds = int(float64(remainingBytes) / bytesPerSecond) } } }) } // GetActiveDownloads retrieves all active download jobs func (dc *DownloadCache) GetActiveDownloads(ctx context.Context) ([]DownloadProgress, error) { pattern := dc.keys.Build(PrefixDownload, "job", "*") keys, err := dc.service.Keys(ctx, pattern) if err != nil { return nil, err } downloads := make([]DownloadProgress, 0, len(keys)) for _, key := range keys { var progress DownloadProgress if err := dc.service.Get(ctx, key, &progress); err != nil { continue } // Only include active downloads if progress.Status == "downloading" || progress.Status == "preparing" { downloads = append(downloads, progress) } } return downloads, nil } // CleanupStaleProgress removes progress entries that haven't been updated recently func (dc *DownloadCache) CleanupStaleProgress(ctx context.Context, maxAge time.Duration) error { pattern := dc.keys.Build(PrefixDownload, "job", "*") keys, err := dc.service.Keys(ctx, pattern) if err != nil { return err } now := time.Now().UTC() toDelete := make([]string, 0) for _, key := range keys { var progress DownloadProgress if err := dc.service.Get(ctx, key, &progress); err != nil { continue } if now.Sub(progress.UpdatedAt) > maxAge { toDelete = append(toDelete, key) } } if len(toDelete) > 0 { return dc.service.Delete(ctx, toDelete...) } return nil } // BulkSetProgress stores multiple download progress entries at once func (dc *DownloadCache) BulkSetProgress(ctx context.Context, progressList []DownloadProgress) error { if len(progressList) == 0 { return nil } pairs := make(map[string]interface{}, len(progressList)) for _, progress := range progressList { key := dc.keys.DownloadJobKey(progress.JobID) progress.UpdatedAt = time.Now().UTC() pairs[key] = progress } if err := dc.service.MSet(ctx, pairs); err != nil { return err } // Set TTL for each key for key := range pairs { if err := dc.service.Expire(ctx, key, TTLDownload); err != nil { return fmt.Errorf("failed to set TTL for %s: %w", key, err) } } return nil }