veza/veza-backend-api/internal/services/cdn_service.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
}