class CloudflareR2Uploader { private string $accountId; private string $accessKey; private string $secretKey; private string $bucketName; private array $mimeOverrides = [ 'pdf' => 'application/pdf', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'svg' => 'image/svg+xml', 'html' => 'text/html', 'txt' => 'text/plain' ]; public function __construct(string $accountId, string $accessKey, string $secretKey, string $bucketName) { $this->accountId = $accountId; $this->accessKey = $accessKey; $this->secretKey = $secretKey; $this->bucketName = $bucketName; } public function uploadFromFile(string $filePath, string $objectKey): string { return $this->upload($filePath, $objectKey); } public function uploadFromUrl(string $imageUrl, string $objectKey = null): string { $imageData = $this->downloadImage($imageUrl); if ($objectKey === null) { $objectKey = $this->deriveObjectKeyFromUrl($imageUrl); } return $this->uploadData($imageData, $objectKey); } private function downloadImage(string $url): string { $ch = curl_init($url); if ($ch === false) { throw new Exception("Failed to initialize cURL."); } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, ]); $data = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { throw new Exception("cURL Error while downloading image: $error"); } if ($httpCode !== 200) { throw new Exception("Failed to download image. HTTP Status Code: $httpCode"); } return $data; } private function deriveObjectKeyFromUrl(string $url): string { $path = parse_url($url, PHP_URL_PATH); $fileName = basename($path); if (empty($fileName)) { $fileName = uniqid('image_', true) . '.jpg'; } return $fileName; } private function uploadData(string $data, string $objectKey): string { $url = "https://{$this->accountId}.r2.cloudflarestorage.com/{$this->bucketName}/$objectKey"; $payloadHash = hash('sha256', $data); $mimeType = $this->getMimeTypeFromData($data, $objectKey); $timestamp = gmdate('Ymd\THis\Z'); $date = gmdate('Ymd'); $canonicalRequest = $this->createCanonicalRequest('PUT', $objectKey, '', $payloadHash, $timestamp); $signature = $this->generateSignature($canonicalRequest, $timestamp, $date); $response = $this->sendRequest($url, $data, $signature, $timestamp, $payloadHash, $mimeType); if ($response['httpCode'] >= 200 && $response['httpCode'] < 300) { return $url; } else { throw new Exception("Failed to upload file. HTTP Code: {$response['httpCode']}. Response: {$response['response']}"); } } public function upload(string $filePath, string $objectKey): string { if (!filter_var($filePath, FILTER_VALIDATE_URL)) { if (!file_exists($filePath)) { throw new Exception("File does not exist: $filePath"); } $fileContent = file_get_contents($filePath); if ($fileContent === false) { throw new Exception("Unable to read the file: $filePath"); } return $this->uploadData($fileContent, $objectKey); } else { return $this->uploadFromUrl($filePath, $objectKey); } } private function getMimeTypeFromData(string $data, string $filePath): string { $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); if (array_key_exists($extension, $this->mimeOverrides)) { return $this->mimeOverrides[$extension]; } $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->buffer($data); return $mimeType ?: 'application/octet-stream'; } private function createCanonicalRequest(string $method, string $objectKey, string $queryString, string $payloadHash, string $timestamp): string { $path = "/{$this->bucketName}/$objectKey"; $canonicalHeaders = [ 'host' => "{$this->accountId}.r2.cloudflarestorage.com", 'x-amz-content-sha256' => $payloadHash, 'x-amz-date' => $timestamp ]; if ($method === 'PUT') { $canonicalHeaders['x-amz-acl'] = 'public-read'; } ksort($canonicalHeaders); $canonicalHeadersStr = ''; foreach ($canonicalHeaders as $key => $value) { $canonicalHeadersStr .= strtolower($key) . ':' . trim($value) . "\n"; } $signedHeaders = implode(';', array_keys($canonicalHeaders)); $signedHeaders = strtolower($signedHeaders); return "$method\n$path\n$queryString\n$canonicalHeadersStr\n$signedHeaders\n$payloadHash"; } private function generateSignature(string $canonicalRequest, string $timestamp, string $date): string { $region = "auto"; $service = "s3"; $credentialScope = "$date/$region/$service/aws4_request"; $hashCanonicalRequest = hash('sha256', $canonicalRequest); $stringToSign = "AWS4-HMAC-SHA256\n$timestamp\n$credentialScope\n$hashCanonicalRequest"; $kDate = hash_hmac('sha256', $date, "AWS4{$this->secretKey}", 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); return "AWS4-HMAC-SHA256 Credential={$this->accessKey}/$credentialScope, SignedHeaders=host;x-amz-acl;x-amz-content-sha256;x-amz-date, Signature=$signature"; } private function sendRequest(string $url, string $fileContent, string $authHeader, string $timestamp, string $payloadHash, string $mimeType): array { $curl = curl_init(); if ($curl === false) { throw new Exception("Failed to initialize cURL for upload."); } curl_setopt_array($curl, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => $fileContent, CURLOPT_HTTPHEADER => [ "Authorization: $authHeader", "x-amz-acl: public-read", "x-amz-content-sha256: $payloadHash", "x-amz-date: $timestamp", "Content-Type: $mimeType", "Content-Disposition: inline", "Content-Length: " . strlen($fileContent) ], CURLOPT_SSL_VERIFYPEER => true, ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $error = curl_error($curl); curl_close($curl); if ($error) { throw new Exception("cURL Error during upload: $error"); } return ['response' => $response, 'httpCode' => $httpCode]; } }
Usage Example
function cloudflare_upload($url, $key) { $uploader = new CloudflareR2Uploader( $accountId, $accessKey, $accessSecret, $bucket ); try { return $uploader->uploadFromUrl($url, $key); } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } }