265 lines
7.4 KiB
Go
265 lines
7.4 KiB
Go
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
|
|
}
|