R2 Setup

EPGOAT Documentation - AI Reference (Educational)

Cloudflare R2 Storage Setup

Status: Active Last Updated: 2025-01-06 Related Docs: System Overview, EPG Generation Project: Architecture & Workflow Redesign (Phase 1)


Overview

EPGOAT uses Cloudflare R2 for EPG file storage. R2 is S3-compatible object storage with zero egress fees (no cost for downloads).

Why R2? - ✅ Zero egress fees (unlimited downloads at no cost) - ✅ S3-compatible API (works with boto3, AWS SDKs) - ✅ Global CDN built-in (fast worldwide delivery) - ✅ Free tier: 10GB storage, 1M Class A ops, 10M Class B ops/month - ✅ Perfect for serving EPG XML files to users

Cost Savings: At 1,000 users with 1M EPG downloads/month: - R2: $0.15/month (storage only, egress FREE) - AWS S3 + CloudFront: $90/month (egress charges) - Savings: $1,080/year


Bucket Details

Bucket Configuration: - Name: epgoat-epg-files - Region: Automatic (Cloudflare globally distributes) - Purpose: Store generated EPG XML files - Access: Private (authenticated access via API tokens)

Dashboard Access: - https://dash.cloudflare.com/[ACCOUNT_ID]/r2/overview - Direct bucket: https://dash.cloudflare.com/[ACCOUNT_ID]/r2/epgoat-epg-files


Environment Configuration

Required Environment Variables

All credentials stored in .env.supabase (NOT committed to git):

# Cloudflare R2 Configuration
R2_ACCOUNT_ID=e7cdd1acc0daca237f0b1e9aa80fc2da
R2_ACCESS_KEY_ID=fe3b39f0b6d9bf32c6b52407f22c365b
R2_SECRET_ACCESS_KEY=e307c1cc6ac107a14e374c0f38c376130b993fca3e179571e37c1f8b70de573b
R2_BUCKET_NAME=epgoat-epg-files
R2_ENDPOINT_URL=https://e7cdd1acc0daca237f0b1e9aa80fc2da.r2.cloudflarestorage.com
R2_PUBLIC_URL=https://epgoat-epg-files.e7cdd1acc0daca237f0b1e9aa80fc2da.r2.cloudflarestorage.com

API Token Permissions

Current token: epgoat-epg-generator - Permissions: Object Read & Write - Scope: epgoat-epg-files bucket only - Created: 2025-01-06 - Expires: Never (or as configured)

Security: Token has minimal permissions (read/write only to specific bucket)


Python Usage

Basic Upload/Download

import boto3
import os
from dotenv import load_dotenv

load_dotenv('.env.supabase')

# Create R2 client (S3-compatible)
s3 = boto3.client(
    's3',
    endpoint_url=os.getenv('R2_ENDPOINT_URL'),
    aws_access_key_id=os.getenv('R2_ACCESS_KEY_ID'),
    aws_secret_access_key=os.getenv('R2_SECRET_ACCESS_KEY'),
    region_name='auto'
)

bucket_name = os.getenv('R2_BUCKET_NAME')

# Upload file
with open('epg.xml', 'rb') as f:
    s3.put_object(
        Bucket=bucket_name,
        Key='tps/tier-top/america-new_york.xml',
        Body=f,
        ContentType='application/xml'
    )

# Download file
obj = s3.get_object(Bucket=bucket_name, Key='tps/tier-top/america-new_york.xml')
content = obj['Body'].read()

# List files
response = s3.list_objects_v2(Bucket=bucket_name, Prefix='tps/')
for obj in response.get('Contents', []):
    print(f"{obj['Key']} - {obj['Size']} bytes")

EPG File Management

def upload_epg_file(provider_slug, tier, timezone, xml_content):
    """Upload EPG file to R2 with standard naming"""
    key = f"{provider_slug}/tier-{tier}/tz-{timezone.replace('/', '-')}.xml"

    s3.put_object(
        Bucket=bucket_name,
        Key=key,
        Body=xml_content.encode('utf-8'),
        ContentType='application/xml',
        CacheControl='public, max-age=3600',  # Cache for 1 hour
        Metadata={
            'provider': provider_slug,
            'tier': tier,
            'timezone': timezone,
            'generated_at': datetime.now(UTC).isoformat()
        }
    )

    return key

def delete_old_epg_files(provider_slug, tier):
    """Clean up old EPG files for a provider/tier"""
    prefix = f"{provider_slug}/tier-{tier}/"
    response = s3.list_objects_v2(Bucket=bucket_name, Prefix=prefix)

    for obj in response.get('Contents', []):
        s3.delete_object(Bucket=bucket_name, Key=obj['Key'])

File Organization

Directory Structure

epgoat-epg-files/
├── tps/                          # Provider: TPS
│   ├── tier-basic/
│   │   ├── tz-america-new_york.xml
│   │   ├── tz-america-chicago.xml
│   │   └── tz-europe-london.xml
│   ├── tier-mid/
│   │   └── ...
│   └── tier-top/
│       └── ...
├── necro/                        # Provider: Necro
│   └── ...
├── test/                         # Test files (temporary)
│   └── connection-test.txt
└── archive/                      # Old files (manual cleanup)
    └── ...

File Naming Convention

Format: {provider_slug}/tier-{tier}/tz-{timezone}.xml

Examples: - tps/tier-basic/tz-america-new_york.xml - tps/tier-top/tz-europe-london.xml - necro/tier-mid/tz-asia-tokyo.xml

Timezone Encoding: - Replace / with - in timezone names - Examples: - America/New_Yorktz-america-new_york.xml - Europe/Londontz-europe-london.xml - Asia/Tokyotz-asia-tokyo.xml


Free Tier Limits

Current Plan: Free - Storage: 10GB/month (first 10GB free) - Class A Operations (uploads, PUT, LIST): 1 million/month free - Class B Operations (downloads, GET): 10 million/month free - Egress: $0 (unlimited, always free)

Current Usage (as of 2025-01-06): - Storage: ~0 MB (bucket just created) - Operations: ~0 (testing phase)

Projected Usage (at 1,000 users): - Storage: ~500MB (450 pre-generated files × ~1MB avg + custom configs) - Class A ops: ~50k/month (uploads during regeneration) - Class B ops: ~100k/month (EPG downloads) - Cost: $0.007/month (well within free tier)


Cost Analysis

Pricing Breakdown

Storage: - First 10GB: FREE - After 10GB: $0.015/GB/month

Class A Operations (uploads, PUT, LIST): - First 1M/month: FREE - After 1M: $4.50/million

Class B Operations (downloads, GET): - First 10M/month: FREE - After 10M: $0.36/million

Egress (data transfer out): - $0 (always free, unlimited)

Scaling Projections

Users Storage Class B Ops Monthly Cost
100 100MB 10k $0
1,000 500MB 100k $0
5,000 1GB 500k $0
10,000 2GB 1M $0
50,000 5GB 5M $0.075
100,000 10GB 10M $0 (at limit)
200,000 15GB 20M $0.43

Note: EPGOAT will stay within free tier until ~100k users!


Cloudflare Workers Integration

Serving EPG Files

R2 integrates perfectly with Cloudflare Workers for URL routing:

// workers/epg-router.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Extract access key from URL: https://epgo.at/G7dK4r
    const accessKey = url.pathname.slice(1);

    // Look up key in Supabase (get provider, tier, timezone)
    const keyInfo = await lookupAccessKey(accessKey);

    if (!keyInfo) {
      return new Response('Invalid key', { status: 403 });
    }

    // Construct R2 object key
    const r2Key = `${keyInfo.provider}/tier-${keyInfo.tier}/tz-${keyInfo.timezone}.xml`;

    // Fetch from R2
    const object = await env.R2_BUCKET.get(r2Key);

    if (!object) {
      return new Response('EPG not found', { status: 404 });
    }

    // Return EPG file
    return new Response(object.body, {
      headers: {
        'Content-Type': 'application/xml',
        'Cache-Control': 'public, max-age=3600'
      }
    });
  }
};

Monitoring & Analytics

Check Bucket Usage

Via Dashboard: 1. Go to Cloudflare Dashboard → R2 2. Click on epgoat-epg-files bucket 3. View Metrics tab 4. See storage usage, operation counts, bandwidth

Via Python:

# List all objects and calculate total size
response = s3.list_objects_v2(Bucket=bucket_name)
total_size = sum(obj['Size'] for obj in response.get('Contents', []))
print(f"Total storage: {total_size / 1024 / 1024:.2f} MB")

Set Up Alerts

In Cloudflare Dashboard: 1. Go to Notifications 2. Create alert for: - Storage approaching 9GB (90% of free tier) - Class A ops approaching 900k/month - Class B ops approaching 9M/month


Backup & Disaster Recovery

Backup Strategy

R2 provides: - Automatic replication across Cloudflare's global network - 99.999999999% (11 9's) durability - No single point of failure

Additional backups (recommended):

# Daily backup to S3 or local storage
def backup_r2_to_local():
    """Download all EPG files for backup"""
    response = s3.list_objects_v2(Bucket=bucket_name)

    for obj in response.get('Contents', []):
        key = obj['Key']
        local_path = f"backups/r2/{key}"

        os.makedirs(os.path.dirname(local_path), exist_ok=True)

        s3.download_file(bucket_name, key, local_path)

    print("Backup complete!")

Disaster Recovery

If R2 becomes unavailable: 1. EPG files can be regenerated from database (epg_data table) 2. Regeneration takes ~5-20 seconds per file 3. Cloudflare has 99.9%+ uptime SLA 4. Use backup from local storage if needed


Troubleshooting

Access Denied Errors

Problem: AccessDenied when uploading/downloading - Cause: Incorrect API credentials or insufficient permissions - Solution: - Verify R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY in .env.supabase - Check token permissions in Cloudflare dashboard - Ensure token has "Object Read & Write" for the bucket

Bucket Not Found

Problem: NoSuchBucket error - Cause: Bucket doesn't exist or incorrect bucket name - Solution: - Verify bucket exists in R2 dashboard - Check R2_BUCKET_NAME matches exactly (case-sensitive) - Ensure account ID in R2_ENDPOINT_URL is correct

Connection Timeout

Problem: Timeout connecting to R2 - Cause: Network issues or firewall blocking - Solution: - Check internet connection - Try different network - Verify R2_ENDPOINT_URL format is correct


Security Best Practices

API Credentials: - ✅ Never commit .env.supabase to git - ✅ Use minimum required permissions (read/write only) - ✅ Rotate API keys every 90 days - ❌ Never expose keys in client-side code - ❌ Never share keys publicly

Bucket Security: - ✅ Keep bucket private (not publicly accessible) - ✅ Use signed URLs for temporary public access if needed - ✅ Enable access logging for audit trail - ❌ Never make bucket publicly writable

Data Security: - EPG files are not sensitive (no PII, no credentials) - Files are served publicly via Workers (authenticated via access keys) - Consider encryption at rest for highly sensitive data (not needed for EPG)


Migration from Other Storage

From AWS S3

# Copy files from S3 to R2
import boto3

# S3 client
s3_source = boto3.client('s3')

# R2 client
s3_dest = boto3.client(
    's3',
    endpoint_url=os.getenv('R2_ENDPOINT_URL'),
    aws_access_key_id=os.getenv('R2_ACCESS_KEY_ID'),
    aws_secret_access_key=os.getenv('R2_SECRET_ACCESS_KEY'),
    region_name='auto'
)

# Copy all files
response = s3_source.list_objects_v2(Bucket='old-epg-bucket')
for obj in response.get('Contents', []):
    copy_source = {'Bucket': 'old-epg-bucket', 'Key': obj['Key']}
    s3_dest.copy(copy_source, os.getenv('R2_BUCKET_NAME'), obj['Key'])


Support

Cloudflare Resources: - Dashboard: https://dash.cloudflare.com - R2 Docs: https://developers.cloudflare.com/r2/ - Support: https://dash.cloudflare.com/?to=/:account/support - Status: https://www.cloudflarestatus.com/

EPGOAT Internal: - Questions? Check #infrastructure channel - Issues? Create ticket with "r2" or "storage" label - Credentials lost? Check password manager