cloudflare_stream-8.x-1.0/src/Tus/UploadClient.php
src/Tus/UploadClient.php
<?php
namespace Drupal\cloudflare_stream\Tus;
use Drupal\Core\Utility\Error;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Log\LoggerInterface;
/**
* A TUS client tailored to our use.
*
* The most commonly used TUS client in packagist is too opinionated to work
* with Cloudflare Streams as it insists on doing the work of requesting the
* upload location.
*/
class UploadClient {
const CHUNK_SIZE = 52428800;
/**
* May be substituted for testing via ::setHttpClient().
*/
protected Client $httpClient;
public function __construct(
protected readonly string $baseUrl,
protected readonly string $filePath,
protected readonly string $apiToken,
protected readonly LoggerInterface $logger,
protected readonly int $chunkSize = self::CHUNK_SIZE,
) {
$this->httpClient = new Client();
}
/**
* Override the client for testing.
*
* @param \GuzzleHttp\Client $httpClient
* A client.
*/
public function setHttpClient(Client $httpClient): void {
$this->httpClient = $httpClient;
}
/**
* Performs the TUS upload.
*/
public function upload() {
if (file_exists($this->filePath) === FALSE) {
throw new \RuntimeException("File $this->filePath does not exist.");
}
// Initialize variables.
$info = @stat($this->filePath);
$size = $info['size'];
$offset = 0;
$handle = fopen($this->filePath, 'r');
if ($handle === FALSE) {
throw new \RuntimeException("Unable to read $this->filePath");
}
// Now upload the file.
$remainder = $size;
do {
$bytes = min($remainder, $this->chunkSize);
// Move file pointer to the offset.
$position = fseek($handle, $offset);
if ($position === -1) {
throw new \RuntimeException("Unable to move internal file pointer to requested location.");
}
// Read the chunk.
$data = fread($handle, $bytes);
if ($data === FALSE) {
throw new \RuntimeException("Cannot read the file data.");
}
// Send the chunk.
try {
$response = $this->httpClient->patch($this->baseUrl, [
'body' => $data,
'headers' => $this->calculateHeaders($bytes, $offset),
]);
if ($response->getStatusCode() === 204) {
$expectedOffset = $offset + $bytes;
$reportedOffsetHeaders = $response->getHeader('Upload-Offset');
$reportedOffset = (int) reset($reportedOffsetHeaders);
if ($reportedOffset !== $expectedOffset) {
throw new \RuntimeException("Cloudflare reported offset of $reportedOffset but $expectedOffset was expected.");
}
// Everything checks out: advance the offset.
$offset = $expectedOffset;
}
}
catch (GuzzleException $e) {
Error::logException($this->logger, $e, 'Recovering from Guzzle error in \Drupal\cloudflare_stream\Tus\UploadClient::upload()');
}
$remainder = $size - $offset;
} while ($remainder > 0);
// Close the file.
fclose($handle);
}
/**
* Prepare the headers.
*
* @param int $bytes
* The size of the payload.
* @param int $offset
* The current offset.
*
* @return mixed[]
* The headers.
*/
protected function calculateHeaders(int $bytes, int $offset): array {
return [
'Tus-Resumable' => '1.0.0',
'Content-Type' => 'application/offset+octet-stream',
'Content-Length' => $bytes,
'Upload-Offset' => $offset,
'Authorization' => 'Bearer ' . $this->apiToken,
];
}
}
