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";
}
}