feat: Configure Cloudflare R2 Object Storage & CDN integration with AWS SigV4 upload and public distribution URL
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user