Files
SEEN/backend/internal/integrations/cache/download_cache.go
T
2026-04-10 12:06:24 +02:00

210 lines
6.1 KiB
Go

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
}