feat: Configure Cloudflare R2 Object Storage & CDN integration with AWS SigV4 upload and public distribution URL

This commit is contained in:
Hamza-Ayed
2026-08-28 02:31:41 +03:00
parent f0592466f6
commit 7b778a1879
+81
View File
@@ -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;
}
}