Skip to content

Commit 81832f9

Browse files
committed
Introduce new, lightweight tus client impl
1 parent 55dfb97 commit 81832f9

5 files changed

Lines changed: 146 additions & 58 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"php": ">=7.1.0",
1616
"ext-curl": "*",
1717
"ext-json":"*",
18-
"ankitpokhrel/tus-php": "^v1.0.0 || ^v2.0.0"
18+
"guzzlehttp/guzzle": "^7.2",
19+
"psr/http-message": "^1.1 || ^2.0"
1920
},
2021
"autoload": {
2122
"psr-4": {

src/Vimeo/Exceptions/VimeoUploadException.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,16 @@
66
*/
77
class VimeoUploadException extends \Exception implements ExceptionInterface
88
{
9+
protected bool $retryable;
10+
11+
public function __construct(string $message = '', int $code = 0, bool $retryable = false)
12+
{
13+
parent::__construct($message, $code);
14+
$this->retryable = $retryable;
15+
}
16+
17+
public function isRetryable()
18+
{
19+
return $this->retryable;
20+
}
921
}

src/Vimeo/Upload/TusClient.php

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,123 @@
33

44
namespace Vimeo\Upload;
55

6+
use GuzzleHttp\Client as GuzzleClient;
7+
use Vimeo\Exceptions\VimeoUploadException;
8+
use Psr\Http\Message\ResponseInterface;
69

7-
8-
class TusClient extends \TusPhp\Tus\Client
10+
class TusClient
911
{
10-
/**
11-
* Sets the url for retrieving the TUS upload.
12-
* @param string $url
13-
* @return $this
14-
*/
15-
public function setUrl(string $url)
12+
public const SUCCESS_STATUS_CODES = [
13+
200,
14+
204
15+
];
16+
public const TUS_PROTOCOL_VERSION = '1.0.0';
17+
protected const HEADER_CONTENT_TYPE = 'application/offset+octet-stream';
18+
19+
/** @var GuzzleClient */
20+
protected $guzzle_client;
21+
22+
/** @var string */
23+
protected $url;
24+
25+
/** @var string */
26+
protected $file_path;
27+
28+
/** @var array<string, string> */
29+
protected $default_headers = ['Tus-Resumable' => self::TUS_PROTOCOL_VERSION];
30+
31+
public function __construct(string $url, string $file_path)
1632
{
1733
$this->url = $url;
18-
return $this;
34+
$this->guzzle_client = new GuzzleClient(['http_errors' => 'false']);
35+
$this->file_path = $file_path;
36+
}
37+
38+
public function upload(int $bytes = 0): int
39+
{
40+
$bytes = $bytes < 0 ? 0 : $bytes;
41+
42+
$offset = $this->sendHeadRequest();
43+
44+
return $this->sendPatchRequest($bytes, $offset);
45+
}
46+
47+
protected function sendHeadRequest(): int
48+
{
49+
$response = $this->sendAndValidateRequest('HEAD', [
50+
'headers' => $this->default_headers + ['http_errors' => false]
51+
]);
52+
53+
return (int) current($response->getHeader('upload-offset'));
54+
}
55+
56+
protected function sendPatchRequest(int $bytes, int $offset): int
57+
{
58+
$file_data = $this->getFileData($bytes, $offset);
59+
60+
$headers = $this->default_headers + [
61+
'Content-Type' => self::HEADER_CONTENT_TYPE,
62+
'Content-Length' => \strlen($file_data),
63+
'Upload-Offset' => $offset,
64+
];
65+
66+
$response = $this->sendAndValidateRequest('PATCH', [
67+
'body' => $file_data,
68+
'headers' => $headers,
69+
]);
70+
71+
return (int) current($response->getHeader('upload-offset'));
72+
}
73+
74+
protected function sendAndValidateRequest(string $method, array $options = []): ResponseInterface
75+
{
76+
77+
$response = $this->guzzle_client->request($method, $this->url, $options);
78+
79+
$body = $response->getBody()->getContents();
80+
$status_code = $response->getStatusCode();
81+
82+
if (in_array($status_code, self::SUCCESS_STATUS_CODES)) {
83+
return $response;
84+
}
85+
86+
$retryable = $status_code === 429 || (500 <= $status_code && $status_code < 600);
87+
if ($retryable) {
88+
throw new VimeoUploadException("$method request to $this->url failed", $status_code, true);
89+
}
90+
91+
throw new VimeoUploadException("$method request to $this->url failed", $status_code);
92+
}
93+
94+
protected function getFileData(int $bytes, int $offset): string
95+
{
96+
if (!file_exists($this->file_path)) {
97+
throw new VimeoUploadException(
98+
"Cannot upload file {$this->file_path}: file does not exist"
99+
);
100+
}
101+
102+
$handle = @fopen($this->file_path, 'rb');
103+
104+
if ($handle === false) {
105+
throw new VimeoUploadException("Cannot open {$this->file_path}");
106+
}
107+
108+
$position = fseek($handle, $offset, SEEK_SET);
109+
if ($position === -1) {
110+
throw new VimeoUploadException(
111+
"Error seeking to position {$offset} in file {$this->file_path}"
112+
);
113+
}
114+
115+
$data = fread($handle, $bytes);
116+
117+
if ($data === false) {
118+
throw new VimeoUploadException("Error reading file {$this->file_path}");
119+
}
120+
121+
fclose($handle);
122+
123+
return $data;
19124
}
20-
}
125+
}

src/Vimeo/Upload/TusClientFactory.php

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/Vimeo/Vimeo.php

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Vimeo\Exceptions\VimeoException;
66
use Vimeo\Exceptions\VimeoRequestException;
77
use Vimeo\Exceptions\VimeoUploadException;
8-
use Vimeo\Upload\TusClientFactory;
8+
use Vimeo\Upload\TusClient;
99

1010
/**
1111
* Copyright 2013 Vimeo
@@ -36,6 +36,7 @@ class Vimeo
3636
const VERSIONS_ENDPOINT = '/versions';
3737
const VERSION_STRING = 'application/vnd.vimeo.*+json; version=3.4';
3838
const USER_AGENT = 'vimeo.php 3.0.8; (http://developer.vimeo.com/api/docs)';
39+
const MAX_BACKOFF = 8;
3940

4041
/** @var array */
4142
protected $_curl_opts = array();
@@ -52,18 +53,14 @@ class Vimeo
5253
/** @var null|string */
5354
private $_access_token = null;
5455

55-
/** @var TusClientFactory */
56-
private $_tus_client_factory = null;
57-
5856
/**
5957
* Creates the Vimeo library, and tracks the client and token information.
6058
*
6159
* @param string $client_id Your applications client id. Can be found on developer.vimeo.com/apps
6260
* @param string $client_secret Your applications client secret. Can be found on developer.vimeo.com/apps
6361
* @param string|null $access_token Your access token. Can be found on developer.vimeo.com/apps or generated using OAuth 2.
64-
* @param TusClientFactory|null $tus_client_interface Your tus client that will be used.
6562
*/
66-
public function __construct(string $client_id, string $client_secret, ?string $access_token = null, ?TusClientFactory $tus_client_factory = null)
63+
public function __construct(string $client_id, string $client_secret, ?string $access_token = null)
6764
{
6865
$this->_client_id = $client_id;
6966
$this->_client_secret = $client_secret;
@@ -76,7 +73,6 @@ public function __construct(string $client_id, string $client_secret, ?string $a
7673
//Certificate must indicate that the server is the server to which you meant to connect.
7774
CURLOPT_SSL_VERIFYHOST => 2,
7875
);
79-
$this->_tus_client_factory = $tus_client_factory ?? new TusClientFactory();
8076
}
8177

8278
/**
@@ -503,7 +499,7 @@ public function uploadTexttrack($texttracks_uri, $file_path, $track_type, $langu
503499

504500
curl_close($curl);
505501
fclose($handle);
506-
502+
507503
if ($curl_info['http_code'] !== 200) {
508504
throw new VimeoUploadException($response);
509505
}
@@ -590,37 +586,31 @@ private function perform_upload_tus(string $file_path, $file_size, array $attemp
590586
}
591587

592588
$url = $attempt['body']['upload']['upload_link'];
593-
$url_path = parse_url($url)['path'];
594-
595-
$base_url = str_replace($url_path, '', $url);
596-
$api_path = $url_path;
597-
$api_pathp = explode('/', $api_path);
598-
$key = $api_pathp[count($api_pathp) - 1];
599-
$api_path = str_replace('/' . $key, '', $api_path);
600589

601590
$bytes_uploaded = 0;
602591
$failures = 0;
603592

604-
$client = $this->_tus_client_factory->getTusClient($base_url, $url);
605-
$client->setApiPath($api_path);
606-
$client->setKey($key)->file($file_path);
607-
$client->getCache()->set($client->getKey(),[
608-
'location' => $url,
609-
'expires_at' => Carbon::now()->addSeconds($client->getCache()->getTtl())->format($client->getCache()::RFC_7231),
610-
]);
593+
$tus_client = new TusClient($url, $file_path);
611594

612595
do {
613596
try {
614-
$bytes_uploaded = $client->upload($chunk_size);
597+
$bytes_uploaded = $tus_client->upload($chunk_size);
615598
} catch (\Exception $e) {
616-
// We likely experienced a timeout, but if we experience three in a row, then we should back off and
617-
// fail so as to not overwhelm servers that are, probably, down.
618-
if ($failures >= 3) {
599+
600+
if ($e instanceof VimeoUploadException && !$e->isRetryable()) {
601+
throw $e;
602+
}
603+
// Maximum retry limit of 10. Will retry for timeouts or connection errors.
604+
if ($failures >= 10) {
619605
throw new VimeoUploadException($e->getMessage());
620606
}
621607

622608
$failures++;
623-
sleep((int)pow(4, $failures)); // sleep 4, 16, 64 seconds (based on failure count)
609+
$backoff = (int)pow(2, $failures - 1);
610+
if ($backoff > self::MAX_BACKOFF) {
611+
$backoff = self::MAX_BACKOFF;
612+
}
613+
sleep($backoff); // sleep 2, 4, 8, 8... seconds (based on failure count)
624614
}
625615
} while ($bytes_uploaded < $file_size);
626616

0 commit comments

Comments
 (0)