Delete Files to Cloudflare R2 Storage

class CloudflareR2Deleter {
    private $accountId;
    private $accessKey;
    private $secretKey;
    private $bucketName;
    private $region = 'auto';
    private $service = 's3';

    public function __construct($accountId, $accessKey, $secretKey, $bucketName) {
        $this->accountId = $accountId;
        $this->accessKey = $accessKey;
        $this->secretKey = $secretKey;
        $this->bucketName = $bucketName;
    }

    public function delete($objectKey) {
        $encodedKey = $objectKey;
        $url = "https://{$this->accountId}.r2.cloudflarestorage.com/{$this->bucketName}/{$encodedKey}";

        $date = $this->getDateString();
        $timeString = $this->getTimeString();
        $payloadHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
        $host = "{$this->accountId}.r2.cloudflarestorage.com";

        $canonicalHeaders = [
            'host' => $host,
            'x-amz-content-sha256' => $payloadHash,
            'x-amz-date' => $timeString,
            'content-length' => '0'
        ];
        ksort($canonicalHeaders);

        $canonicalHeadersStr = '';
        foreach ($canonicalHeaders as $key => $value) {
            $canonicalHeadersStr .= $key . ':' . trim($value) . "\n";
        }
        $signedHeaders = implode(';', array_keys($canonicalHeaders));

        $canonicalRequest = "DELETE\n/{$this->bucketName}/{$encodedKey}\n\n" .
            rtrim($canonicalHeadersStr, "\n") . "\n\n" .
            $signedHeaders . "\n" .
            $payloadHash;

        $canonicalRequestHash = hash('sha256', $canonicalRequest);

        $stringToSign = "AWS4-HMAC-SHA256\n{$timeString}\n{$this->getCredentialScope()}\n{$canonicalRequestHash}";

        $signature = $this->generateSignature($stringToSign);

        $authorizationHeader = "AWS4-HMAC-SHA256 Credential={$this->accessKey}/{$this->getCredentialScope()}, SignedHeaders={$signedHeaders}, Signature={$signature}";

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => 'DELETE',
            CURLOPT_HEADER => true,
            CURLOPT_HTTPHEADER => [
                "Authorization: {$authorizationHeader}",
                "x-amz-content-sha256: {$payloadHash}",
                "x-amz-date: {$timeString}",
                "Host: {$host}",
                "Content-Length: 0"
            ],
            CURLOPT_VERBOSE => false
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new Exception("cURL Error: $error");
        }

        if ($httpCode !== 204) {
            throw new Exception("Failed to delete file. HTTP Code: $httpCode");
        }

        return true;
    }

    private function generateSignature($stringToSign) {
        $kDate = hash_hmac('sha256', $this->getDateString(), "AWS4{$this->secretKey}", true);
        $kRegion = hash_hmac('sha256', $this->region, $kDate, true);
        $kService = hash_hmac('sha256', $this->service, $kRegion, true);
        $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
        return hash_hmac('sha256', $stringToSign, $kSigning);
    }

    private function getDateString() {
        return gmdate('Ymd');
    }

    private function getTimeString() {
        return gmdate('Ymd\THis\Z');
    }

    private function getCredentialScope() {
        return "{$this->getDateString()}/{$this->region}/{$this->service}/aws4_request";
    }
}

Usage Example:

function cloudflare_delete($key) {
    $deleter = new CloudflareR2Deleter(
        $accountId,
        $accessKey,
        $accessSecret,
        $bucket
    );

    try {

      return $deleter->delete($key);

    } catch (Exception $e) {
        echo "Error: " . $e->getMessage() . "\n";
    }

}