Passed
Push — master ( 12bba4...f7937a )
by Shahrad
01:31
created

Client::get_chunk_size()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
namespace EasyHttp;
4
5
use CurlHandle;
6
use EasyHttp\Enums\ErrorCode;
7
use EasyHttp\Model\DownloadResult;
8
use EasyHttp\Model\HttpOptions;
9
use EasyHttp\Model\HttpResponse;
10
use EasyHttp\Traits\ClientTrait;
11
use EasyHttp\Util\Utils;
12
use InvalidArgumentException;
13
use RuntimeException;
14
15
/**
16
 * Client
17
 *
18
 * @link    https://github.com/shahradelahi/easy-http
19
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
20
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
21
 */
22
class Client
23
{
24
25
    use ClientTrait;
26
27
    /**
28
     * The temp directory to download files - default is $_SERVER['TEMP']
29
     *
30
     * @var ?string
31
     */
32
    private ?string $tempDir;
33
34
    /**
35
     * The Max count of chunk to download file
36
     *
37
     * @var int
38
     */
39
    public int $maxChunkCount = 10;
40
41
    /**
42
     * The constructor of the client
43
     */
44
    public function __construct()
45
    {
46
        $this->tempDir = $_SERVER['TEMP'] ?? null;
47
        $this->setHasSelfSignedCertificate(true);
48
    }
49
50
    /**
51
     * Set has self-signed certificate
52
     *
53
     * This is used to set the curl option CURLOPT_SSL_VERIFYPEER
54
     * and CURLOPT_SSL_VERIFYHOST to false. This is useful when you are
55
     * in local environment, or you have self-signed certificate.
56
     *
57
     * @param bool $has
58
     *
59
     * @return void
60
     */
61
    public function setHasSelfSignedCertificate(bool $has): void
62
    {
63
        putenv('HAS_SELF_SIGNED_CERT='.($has ? 'true' : 'false'));
64
    }
65
66
    /**
67
     * Set the temporary directory path to save the downloaded files
68
     *
69
     * @param string $path
70
     *
71
     * @return void
72
     */
73
    public function setTempPath(string $path): void
74
    {
75
        if (!file_exists($path)) {
76
            throw new InvalidArgumentException(
77
                sprintf('The path "%s" does not exist', $path)
78
            );
79
        }
80
        $this->tempDir = $path;
81
    }
82
83
    /**
84
     * This method is used to send a http request to a given url.
85
     *
86
     * @param string $method
87
     * @param string $uri
88
     * @param array|HttpOptions $options
89
     *
90
     * @return HttpResponse
91
     */
92
    public function request(string $method, string $uri, array|HttpOptions $options = []): HttpResponse
93
    {
94
        $CurlHandle = Middleware::create_curl_handler($method, $uri, $options);
95
        if (!$CurlHandle) {
96
            throw new RuntimeException(
97
                'An error occurred while creating the curl handler'
98
            );
99
        }
100
101
        $result = new HttpResponse();
102
        $result->setCurlHandle($CurlHandle);
103
104
        $response = curl_exec($CurlHandle);
105
        if (curl_errno($CurlHandle) || !$response) {
106
            $result->setErrorCode(curl_errno($CurlHandle));
107
            $result->setErrorMessage(
108
                curl_error($CurlHandle) ??
109
                ErrorCode::getMessage(curl_errno($CurlHandle))
110
            );
111
            return $result;
112
        }
113
114
        $result->setStatusCode(curl_getinfo($CurlHandle, CURLINFO_HTTP_CODE));
115
        $result->setHeaderSize(curl_getinfo($CurlHandle, CURLINFO_HEADER_SIZE));
116
        $result->setHeaders(substr((string) $response, 0, $result->getHeaderSize()));
117
        $result->setBody(substr((string) $response, $result->getHeaderSize()));
118
119
        curl_close($CurlHandle);
120
121
        return $result;
122
    }
123
124
    /**
125
     * Send multiple requests to a given url.
126
     *
127
     * @param array $requests [{method, uri, options}, ...]
128
     *
129
     * @return array<HttpResponse>
130
     */
131
    public function bulk(array $requests): array
132
    {
133
        $result = [];
134
        $handlers = [];
135
        $multi_handler = curl_multi_init();
136
        foreach ($requests as $request) {
137
138
            $CurlHandle = Middleware::create_curl_handler(
139
                $request['method'] ?? null,
140
                $request['uri'],
141
                $request['options'] ?? []
142
            );
143
            if (!$CurlHandle) {
144
                throw new RuntimeException(
145
                    'An error occurred while creating the curl handler'
146
                );
147
            }
148
            $handlers[] = $CurlHandle;
149
            curl_multi_add_handle($multi_handler, $CurlHandle);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_add_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

149
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $multi_handler, $CurlHandle);
Loading history...
150
151
        }
152
153
        $active = null;
154
        do {
155
            $mrc = curl_multi_exec($multi_handler, $active);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_exec() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

155
            $mrc = curl_multi_exec(/** @scrutinizer ignore-type */ $multi_handler, $active);
Loading history...
156
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
157
158
        while ($active && $mrc == CURLM_OK) {
159
            if (curl_multi_select($multi_handler) != -1) {
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_select() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

159
            if (curl_multi_select(/** @scrutinizer ignore-type */ $multi_handler) != -1) {
Loading history...
160
                do {
161
                    $mrc = curl_multi_exec($multi_handler, $active);
162
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
163
            }
164
        }
165
166
        foreach ($handlers as $handler) {
167
            curl_multi_remove_handle($multi_handler, $handler);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_remove_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

167
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $multi_handler, $handler);
Loading history...
168
        }
169
        curl_multi_close($multi_handler);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_close() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

169
        curl_multi_close(/** @scrutinizer ignore-type */ $multi_handler);
Loading history...
170
171
        foreach ($handlers as $handler) {
172
            $content = curl_multi_getcontent($handler);
173
            $response = new HttpResponse();
174
175
            if (curl_errno($handler)) {
176
                $response->setErrorCode(curl_errno($handler));
177
                $response->setErrorMessage(
178
                    curl_error($handler) ??
179
                    ErrorCode::getMessage(curl_errno($handler))
180
                );
181
            }
182
183
            $response->setCurlHandle($handler);
184
            $response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE));
185
            $response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE));
186
            $response->setHeaders(substr($content, 0, $response->getHeaderSize()));
187
            $response->setBody(substr($content, $response->getHeaderSize()));
188
189
            $result[] = $response;
190
        }
191
192
        return $result;
193
    }
194
195
    /**
196
     * Download large files.
197
     *
198
     * This method is used to download large files with creating multiple requests.
199
     *
200
     * Change `max_chunk_count` variable to change the number of chunks. (default: 10)
201
     *
202
     * @param string $url The direct url to the file.
203
     * @param array|HttpOptions $options The options to use.
204
     *
205
     * @return DownloadResult
206
     */
207
    public function download(string $url, array|HttpOptions $options = []): DownloadResult
208
    {
209
        if (empty($this->tempDir)) {
210
            throw new RuntimeException(
211
                'The temp directory is not set. Please set the temp directory using the `setTempDir` method.'
212
            );
213
        }
214
215
        if (!file_exists($this->tempDir)) {
216
            if (mkdir($this->tempDir, 0777, true) === false) {
217
                throw new RuntimeException(
218
                    'The temp directory is not writable. Please set the temp directory using the `setTempDir` method.'
219
                );
220
            }
221
        }
222
223
        if (gettype($options) === 'array') {
224
            $options = new HttpOptions($options);
225
        }
226
227
        $fileSize = $this->get_file_size($url);
228
        $chunkSize = $this->get_chunk_size($fileSize);
229
230
        $result = new DownloadResult();
231
232
        $result->id = uniqid();
233
        $result->chunksPath = $this->tempDir . '/' . $result->id . '/';
234
        mkdir($result->chunksPath, 0777, true);
235
236
        $result->fileSize = $fileSize;
237
        $result->chunkSize = $chunkSize;
238
        $result->chunks = ceil($fileSize / $chunkSize);
239
240
        $result->startTime = time();
241
242
        $requests = [];
243
        for ($i = 0; $i < $result->chunks; $i++) {
244
            $range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize;
245
            if ($i + 1 === $result->chunks) {
246
                $range = $i * $chunkSize . '-' . $fileSize;
247
            }
248
            $requests[] = [
249
                'method' => 'GET',
250
                'uri' => $url,
251
                'options' => array_merge($options->toArray(), [
252
                    'CurlOptions' => [
253
                        CURLOPT_RANGE => $range
254
                    ],
255
                ])
256
            ];
257
        }
258
259
        foreach ($this->bulk($requests) as $response) {
260
            $result->addChunk(
261
                Utils::randomString(16),
262
                $response->getBody(),
263
                $response->getInfoFromCurl()->TOTAL_TIME
264
            );
265
        }
266
267
        $result->endTime = time();
268
269
        return $result;
270
    }
271
272
    /**
273
     * Upload single or multiple files
274
     *
275
     * This method is sending file with request method of POST and
276
     * Content-Type of multipart/form-data.
277
     *
278
     * @param string $url The direct url to the file.
279
     * @param array $filePath The path to the file.
280
     * @param array|HttpOptions $options The options to use.
281
     *
282
     * @return HttpResponse
283
     */
284
    public function upload(string $url, array $filePath, array|HttpOptions $options = []): HttpResponse
285
    {
286
        if (gettype($options) === 'array') {
287
            $options = new HttpOptions($options);
288
        }
289
290
        $multipart = [];
291
292
        foreach ($filePath as $key => $file) {
293
            $multipart[$key] = new \CURLFile(
294
                realpath($file),
295
                $this->get_file_type($file)
296
            );
297
        }
298
299
        $options->setMultipart($multipart);
300
        return $this->post($url, $options);
301
    }
302
303
    /**
304
     * Get filetype with the extension.
305
     *
306
     * @param string $filename The absolute path to the file.
307
     * @return string eg. image/jpeg
308
     */
309
    public static function get_file_type(string $filename): string
310
    {
311
        return MimeType::$TYPES[pathinfo($filename, PATHINFO_EXTENSION)] ?? 'application/octet-stream';
312
    }
313
314
    /**
315
     * Get file size.
316
     *
317
     * @param string $url The direct url to the file.
318
     * @return int
319
     */
320
    public function get_file_size(string $url): int
321
    {
322
        if (file_exists($url)) {
323
            return filesize($url);
324
        }
325
326
        $response = $this->get($url, [
327
            'headers' => [
328
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36',
329
            ],
330
            'CurlOptions' => [
331
                CURLOPT_NOBODY => true,
332
            ]
333
        ]);
334
335
        return (int) $response->getHeaderLine('Content-Length') ?? 0;
336
    }
337
338
    /**
339
     * Get the size of each chunk.
340
     *
341
     * For default, we're dividing filesize to 10 as max size of each chunk.
342
     * If the file size was smaller than 2MB, we'll use the filesize as single chunk.
343
     *
344
     * @param int $fileSize The file size.
345
     * @return int
346
     */
347
    private function get_chunk_size(int $fileSize): int
348
    {
349
        $maxChunkSize = $fileSize / $this->maxChunkCount;
350
351
        if ($fileSize <= 2 * 1024 * 1024) {
352
            return $fileSize;
353
        }
354
355
        return min($maxChunkSize, $fileSize);
356
    }
357
358
}