package services import ( "context" "fmt" "strings" "time" "go.uber.org/zap" ) // CDNProvider represents the type of CDN provider type CDNProvider string const ( CDNProviderCloudFront CDNProvider = "cloudfront" CDNProviderCloudflare CDNProvider = "cloudflare" CDNProviderGeneric CDNProvider = "generic" CDNProviderNone CDNProvider = "none" ) // CDNConfig represents configuration for CDN service type CDNConfig struct { Provider CDNProvider BaseURL string // CDN base URL (e.g., https://d1234567890.cloudfront.net) DistributionID string // CloudFront distribution ID (if applicable) APIKey string // API key for cache invalidation (if applicable) Enabled bool Logger *zap.Logger } // CDNService provides CDN integration capabilities // BE-SVC-013: Implement CDN integration type CDNService struct { config CDNConfig logger *zap.Logger } // NewCDNService creates a new CDN service func NewCDNService(config CDNConfig) *CDNService { if config.Logger == nil { config.Logger = zap.NewNop() } return &CDNService{ config: config, logger: config.Logger, } } // GetURL generates a CDN URL for a given path func (s *CDNService) GetURL(path string) string { if !s.config.Enabled || s.config.Provider == CDNProviderNone { return path // Return original path if CDN is disabled } // Remove leading slash if present path = strings.TrimPrefix(path, "/") // Ensure base URL doesn't have trailing slash baseURL := strings.TrimSuffix(s.config.BaseURL, "/") // Construct CDN URL return fmt.Sprintf("%s/%s", baseURL, path) } // GetURLs generates CDN URLs for multiple paths func (s *CDNService) GetURLs(paths []string) map[string]string { result := make(map[string]string) for _, path := range paths { result[path] = s.GetURL(path) } return result } // GetAssetURL generates a CDN URL for a static asset func (s *CDNService) GetAssetURL(assetType string, filename string) string { path := fmt.Sprintf("assets/%s/%s", assetType, filename) return s.GetURL(path) } // GetAudioURL generates a CDN URL for an audio file func (s *CDNService) GetAudioURL(trackID string, filename string) string { path := fmt.Sprintf("audio/%s/%s", trackID, filename) return s.GetURL(path) } // GetHLSURL generates a CDN URL for an HLS stream func (s *CDNService) GetHLSURL(trackID string, path string) string { fullPath := fmt.Sprintf("hls/%s/%s", trackID, path) return s.GetURL(fullPath) } // GetImageURL generates a CDN URL for an image func (s *CDNService) GetImageURL(imageType string, filename string) string { path := fmt.Sprintf("images/%s/%s", imageType, filename) return s.GetURL(path) } // InvalidateCache invalidates CDN cache for given paths func (s *CDNService) InvalidateCache(ctx context.Context, paths []string) error { if !s.config.Enabled { s.logger.Debug("CDN cache invalidation skipped (CDN disabled)") return nil } if len(paths) == 0 { return nil } switch s.config.Provider { case CDNProviderCloudFront: return s.invalidateCloudFront(ctx, paths) case CDNProviderCloudflare: return s.invalidateCloudflare(ctx, paths) case CDNProviderGeneric: // Generic CDN - just log, actual invalidation would need API integration s.logger.Info("CDN cache invalidation requested (generic provider)", zap.Strings("paths", paths), ) return nil default: s.logger.Warn("CDN cache invalidation not supported for provider", zap.String("provider", string(s.config.Provider)), ) return nil } } // invalidateCloudFront invalidates CloudFront cache func (s *CDNService) invalidateCloudFront(ctx context.Context, paths []string) error { // Note: Full CloudFront invalidation would require AWS SDK // This is a placeholder implementation s.logger.Info("CloudFront cache invalidation requested", zap.String("distribution_id", s.config.DistributionID), zap.Strings("paths", paths), ) // In a real implementation, this would: // 1. Create CloudFront invalidation request // 2. Use AWS SDK to submit invalidation // 3. Return invalidation ID for tracking return nil } // invalidateCloudflare invalidates Cloudflare cache func (s *CDNService) invalidateCloudflare(ctx context.Context, paths []string) error { // Note: Full Cloudflare invalidation would require Cloudflare API // This is a placeholder implementation s.logger.Info("Cloudflare cache invalidation requested", zap.Strings("paths", paths), ) // In a real implementation, this would: // 1. Call Cloudflare API to purge cache // 2. Handle API authentication // 3. Return purge result return nil } // IsEnabled returns whether CDN is enabled func (s *CDNService) IsEnabled() bool { return s.config.Enabled && s.config.Provider != CDNProviderNone } // GetProvider returns the CDN provider type func (s *CDNService) GetProvider() CDNProvider { return s.config.Provider } // GetBaseURL returns the CDN base URL func (s *CDNService) GetBaseURL() string { return s.config.BaseURL } // GenerateSignedURL generates a signed URL for private content (if supported) func (s *CDNService) GenerateSignedURL(path string, expiration time.Duration) (string, error) { if !s.config.Enabled { return s.GetURL(path), nil } switch s.config.Provider { case CDNProviderCloudFront: return s.generateCloudFrontSignedURL(path, expiration) case CDNProviderCloudflare: // Cloudflare doesn't support signed URLs in the same way // Return regular URL return s.GetURL(path), nil default: return s.GetURL(path), nil } } // generateCloudFrontSignedURL generates a CloudFront signed URL func (s *CDNService) generateCloudFrontSignedURL(path string, expiration time.Duration) (string, error) { // Note: Full CloudFront signed URL generation would require AWS SDK // This is a placeholder implementation s.logger.Info("CloudFront signed URL generation requested", zap.String("path", path), zap.Duration("expiration", expiration), ) // In a real implementation, this would: // 1. Use AWS SDK to generate signed URL // 2. Include expiration time // 3. Sign with private key // For now, return regular URL return s.GetURL(path), nil } // GetCacheHeaders returns appropriate cache headers for CDN func (s *CDNService) GetCacheHeaders() map[string]string { headers := make(map[string]string) if !s.config.Enabled { headers["Cache-Control"] = "no-cache" return headers } switch s.config.Provider { case CDNProviderCloudFront: headers["Cache-Control"] = "public, max-age=31536000, immutable" headers["X-CDN-Provider"] = "cloudfront" case CDNProviderCloudflare: headers["Cache-Control"] = "public, max-age=31536000, immutable" headers["X-CDN-Provider"] = "cloudflare" default: headers["Cache-Control"] = "public, max-age=3600" } return headers } // BatchInvalidate invalidates multiple paths in batches (useful for rate limits) func (s *CDNService) BatchInvalidate(ctx context.Context, paths []string, batchSize int) error { if batchSize <= 0 { batchSize = 10 // Default batch size } for i := 0; i < len(paths); i += batchSize { end := i + batchSize if end > len(paths) { end = len(paths) } batch := paths[i:end] if err := s.InvalidateCache(ctx, batch); err != nil { s.logger.Warn("Failed to invalidate cache batch", zap.Int("batch_start", i), zap.Int("batch_end", end), zap.Error(err), ) // Continue with next batch even if one fails } // Small delay between batches to respect rate limits if end < len(paths) { time.Sleep(100 * time.Millisecond) } } return nil }