From 7b778a1879cc375c9d33ae3d68926650015fcbf3 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 28 Aug 2026 02:31:41 +0300 Subject: [PATCH] feat: Configure Cloudflare R2 Object Storage & CDN integration with AWS SigV4 upload and public distribution URL --- backend/app/Services/VideoService.php | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/backend/app/Services/VideoService.php b/backend/app/Services/VideoService.php index 4e70379..82ee8da 100644 --- a/backend/app/Services/VideoService.php +++ b/backend/app/Services/VideoService.php @@ -476,4 +476,85 @@ class VideoService 'expires_at' => $expires ]; } + + // ========================================== + // Cloudflare R2 Object Storage & CDN Engine + // ========================================== + public static function getR2Config(): array + { + return [ + 'account_id' => getenv('CLOUDFLARE_R2_ACCOUNT_ID') ?: 'fd6e6388f69f6b2d7c073861f9bef4ee', + 'access_key' => getenv('CLOUDFLARE_R2_ACCESS_KEY') ?: 'ba4df2726c7e717f2c4b7fff7db0e1ea', + 'secret_key' => getenv('CLOUDFLARE_R2_SECRET_KEY') ?: 'ce5dfeef2b89e4bb2760c2e856006cc201e6218018a5d2bf25188a2ced61687b', + 'endpoint' => getenv('CLOUDFLARE_R2_ENDPOINT') ?: 'https://fd6e6388f69f6b2d7c073861f9bef4ee.r2.cloudflarestorage.com', + 'bucket' => getenv('CLOUDFLARE_R2_BUCKET') ?: 'saqel-media', + 'public_url' => getenv('CLOUDFLARE_R2_PUBLIC_URL') ?: 'https://pub-85b40493a03d464cb6c5b4adb43749f5.r2.dev' + ]; + } + + /** + * Upload any file (video, segment, pdf, thumbnail) to Cloudflare R2 with AWS SigV4 + */ + public static function uploadToR2(string $localFilePath, string $r2Key, string $contentType = 'application/octet-stream'): ?string + { + if (!file_exists($localFilePath)) { + return null; + } + + $config = self::getR2Config(); + $bucket = $config['bucket']; + $host = "{$bucket}.{$config['account_id']}.r2.cloudflarestorage.com"; + $endpoint = "https://{$host}/" . ltrim($r2Key, '/'); + + $payload = file_get_contents($localFilePath); + $payloadHash = hash('sha256', $payload); + + $date = gmdate('Ymd\THis\Z'); + $shortDate = gmdate('Ymd'); + $region = 'auto'; + $service = 's3'; + + // AWS SigV4 Canonical Headers + $canonicalHeaders = "host:{$host}\nx-amz-content-sha256:{$payloadHash}\nx-amz-date:{$date}\n"; + $signedHeaders = 'host;x-amz-content-sha256;x-amz-date'; + + $canonicalRequest = "PUT\n/" . ltrim($r2Key, '/') . "\n\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}"; + $stringToSign = "AWS4-HMAC-SHA256\n{$date}\n{$shortDate}/{$region}/{$service}/aws4_request\n" . hash('sha256', $canonicalRequest); + + // Calculate Signing Key + $kSecret = 'AWS4' . $config['secret_key']; + $kDate = hash_hmac('sha256', $shortDate, $kSecret, true); + $kRegion = hash_hmac('sha256', $region, $kDate, true); + $kService = hash_hmac('sha256', $service, $kRegion, true); + $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); + $signature = hash_hmac('sha256', $stringToSign, $kSigning); + + $authHeader = "AWS4-HMAC-SHA256 Credential={$config['access_key']}/{$shortDate}/{$region}/{$service}/aws4_request, SignedHeaders={$signedHeaders}, Signature={$signature}"; + + $ch = curl_init($endpoint); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => [ + "Host: {$host}", + "x-amz-date: {$date}", + "x-amz-content-sha256: {$payloadHash}", + "Authorization: {$authHeader}", + "Content-Type: {$contentType}", + "Content-Length: " . strlen($payload) + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 300 + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode >= 200 && $httpCode < 300) { + return rtrim($config['public_url'], '/') . '/' . ltrim($r2Key, '/'); + } + + return null; + } }