Skip to content

Commit 76e74fd

Browse files
committed
Improve and add tests for Curl::fastDownload()
1 parent 2bb2f32 commit 76e74fd

File tree

3 files changed

+136
-40
lines changed

3 files changed

+136
-40
lines changed

src/Curl/Curl.php

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -373,19 +373,23 @@ public function download($url, $mixed_filename)
373373
*/
374374
public function fastDownload($url, $filename, $connections = 4)
375375
{
376-
// Retrieve content length from the "Content-Length" header and use an
377-
// HTTP GET request because not all hosts support HEAD requests.
378-
$this->setOpts([
379-
CURLOPT_CUSTOMREQUEST => 'GET',
380-
CURLOPT_NOBODY => true,
381-
CURLOPT_HEADER => true,
382-
CURLOPT_ENCODING => '',
383-
]);
384-
$this->setUrl($url);
385-
$this->exec();
376+
// Retrieve content length from the "Content-Length" header from the url
377+
// to download. Use an HTTP GET request without a body instead of a HEAD
378+
// request because not all hosts support HEAD requests.
379+
$curl = new Curl();
380+
$curl->setOptInternal(CURLOPT_NOBODY, true);
381+
382+
// Pass user-specified options to the instance checking for content-length.
383+
$curl->setOpts($this->userSetOptions);
384+
$curl->get($url);
385+
386+
// Exit early when an error occurred.
387+
if ($curl->error) {
388+
return false;
389+
}
386390

387-
$content_length = isset($this->responseHeaders['Content-Length']) ?
388-
$this->responseHeaders['Content-Length'] : null;
391+
$content_length = isset($curl->responseHeaders['Content-Length']) ?
392+
$curl->responseHeaders['Content-Length'] : null;
389393

390394
// Use a regular download when content length could not be determined.
391395
if (!$content_length) {
@@ -395,25 +399,21 @@ public function fastDownload($url, $filename, $connections = 4)
395399
// Divide chunk_size across the number of connections.
396400
$chunk_size = ceil($content_length / $connections);
397401

398-
// First bytes.
399-
$offset = 0;
400-
$next_chunk = $chunk_size;
401-
402402
// Keep track of file name parts.
403403
$part_file_names = [];
404404

405405
$multi_curl = new MultiCurl();
406406
$multi_curl->setConcurrency($connections);
407-
$multi_curl->error(function ($instance) {
408-
return false;
409-
});
410407

411-
for ($i = 1; $i <= $connections; $i++) {
412-
// If last chunk then no need to supply it.
413-
// Range starts with 0, so subtract 1.
414-
$next_chunk = $i === $connections ? '' : $next_chunk - 1;
408+
for ($part_number = 1; $part_number <= $connections; $part_number++) {
409+
$range_start = ($part_number - 1) * $chunk_size;
410+
$range_end = $range_start + $chunk_size - 1;
411+
if ($part_number === $connections) {
412+
$range_end = '';
413+
}
414+
$range = $range_start . '-' . $range_end;
415415

416-
$part_file_name = $filename . '.part' . $i;
416+
$part_file_name = $filename . '.part' . $part_number;
417417

418418
// Save the file name of this part.
419419
$part_file_names[] = $part_file_name;
@@ -424,25 +424,28 @@ public function fastDownload($url, $filename, $connections = 4)
424424
}
425425

426426
// Create file part.
427-
$file_handle = fopen($part_file_name, 'w');
427+
$file_handle = tmpfile();
428428

429+
// Setup the instance downloading a part.
429430
$curl = new Curl();
430-
$curl->setOpt(CURLOPT_ENCODING, '');
431-
$curl->setRange($offset . '-' . $next_chunk);
432-
$curl->setFile($file_handle);
433-
$curl->disableTimeout(); // otherwise download may fail.
434431
$curl->setUrl($url);
435432

436-
$curl->complete(function () use ($file_handle) {
437-
fclose($file_handle);
438-
});
433+
// Pass user-specified options to the instance downloading a part.
434+
$curl->setOpts($this->userSetOptions);
439435

440-
$multi_curl->addCurl($curl);
436+
$curl->setOptInternal(CURLOPT_CUSTOMREQUEST, 'GET');
437+
$curl->setOptInternal(CURLOPT_HTTPGET, true);
438+
$curl->setRangeInternal($range);
439+
$curl->setFileInternal($file_handle);
440+
$curl->fileHandle = $file_handle;
441441

442-
if ($i !== $connections) {
443-
$offset = $next_chunk + 1; // Add 1 to match offset.
444-
$next_chunk = $next_chunk + $chunk_size;
445-
}
442+
$curl->downloadCompleteCallback = function ($instance, $tmpfile) use ($part_file_name) {
443+
$fh = fopen($part_file_name, 'wb');
444+
stream_copy_to_stream($tmpfile, $fh);
445+
fclose($fh);
446+
};
447+
448+
$multi_curl->addCurl($curl);
446449
}
447450

448451
// Start the simultaneous downloads for each of the ranges in parallel.
@@ -457,7 +460,15 @@ public function fastDownload($url, $filename, $connections = 4)
457460
$main_file_handle = fopen($filename, 'w');
458461

459462
foreach ($part_file_names as $part_file_name) {
463+
if (!is_file($part_file_name)) {
464+
return false;
465+
}
466+
460467
$file_handle = fopen($part_file_name, 'r');
468+
if ($file_handle === false) {
469+
return false;
470+
}
471+
461472
stream_copy_to_stream($file_handle, $main_file_handle);
462473
fclose($file_handle);
463474
unlink($part_file_name);
@@ -1156,6 +1167,9 @@ protected function setOptInternal($option, $value)
11561167
*/
11571168
public function setOpts($options)
11581169
{
1170+
if (!count($options)) {
1171+
return true;
1172+
}
11591173
foreach ($options as $option => $value) {
11601174
if (!$this->setOpt($option, $value)) {
11611175
return false;
@@ -1799,6 +1813,9 @@ function ($key) use ($curl_const_prefix) {
17991813
echo "\n";
18001814
} elseif (is_bool($value)) {
18011815
echo ' ' . ($value ? 'true' : 'false') . "\n";
1816+
} elseif (is_array($value)) {
1817+
echo ' ';
1818+
var_dump($value);
18021819
} elseif (is_callable($value)) {
18031820
echo ' (callable)' . "\n";
18041821
} else {

tests/PHPCurlClass/PHPCurlClassTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,81 @@ public function testDownloadCallbackError()
884884
$this->assertFalse($download_callback_called);
885885
}
886886

887+
public function testFastDownloadSuccessOnly()
888+
{
889+
// Create a local file.
890+
$local_file_path = \Helper\get_png();
891+
892+
// Upload file to server.
893+
$uploaded_server_file_path = \Helper\upload_file_to_server($local_file_path);
894+
895+
// Download server file and save locally.
896+
$url = Test::TEST_URL . '?' . http_build_query([
897+
'file_path' => $uploaded_server_file_path,
898+
]);
899+
$downloaded_local_file_path = \Helper\get_tmp_file_path();
900+
$curl = new Curl();
901+
$curl->setHeader('X-DEBUG-TEST', 'download_response');
902+
$curl->fastDownload($url, $downloaded_local_file_path);
903+
904+
$this->assertEquals(md5_file($local_file_path), md5_file($downloaded_local_file_path));
905+
906+
// Remove server file.
907+
\Helper\remove_file_from_server($uploaded_server_file_path);
908+
909+
unlink($local_file_path);
910+
$this->assertFalse(file_exists($local_file_path));
911+
912+
unlink($downloaded_local_file_path);
913+
$this->assertFalse(file_exists($downloaded_local_file_path));
914+
}
915+
916+
public function testFastDownloadFailOnly()
917+
{
918+
$url = Test::TEST_URL . '?failures=1';
919+
$downloaded_local_file_path = \Helper\get_tmp_file_path();
920+
921+
$curl = new Curl();
922+
$curl->setHeader('X-DEBUG-TEST', 'retry');
923+
$response = $curl->fastDownload($url, $downloaded_local_file_path);
924+
925+
$this->assertFalse($response);
926+
}
927+
928+
public function testFastDownloadSuccessFail()
929+
{
930+
$url = Test::TEST_URL . '?failures=0,1';
931+
$downloaded_local_file_path = \Helper\get_tmp_file_path();
932+
$cookie_jar = __DIR__ . '/cookiejar.txt';
933+
$connections = 1;
934+
935+
$curl = new Curl();
936+
$curl->setHeader('X-DEBUG-TEST', 'retry');
937+
$curl->setCookieFile($cookie_jar);
938+
$curl->setCookieJar($cookie_jar);
939+
$response = $curl->fastDownload($url, $downloaded_local_file_path, $connections);
940+
941+
$this->assertFalse($response);
942+
$this->assertTrue(unlink($cookie_jar));
943+
}
944+
945+
public function testFastDownloadSuccessSuccessFail()
946+
{
947+
$url = Test::TEST_URL . '?failures=0,0,1';
948+
$downloaded_local_file_path = \Helper\get_tmp_file_path();
949+
$cookie_jar = __DIR__ . '/cookiejar.txt';
950+
$connections = 2;
951+
952+
$curl = new Curl();
953+
$curl->setHeader('X-DEBUG-TEST', 'retry');
954+
$curl->setCookieFile($cookie_jar);
955+
$curl->setCookieJar($cookie_jar);
956+
$response = $curl->fastDownload($url, $downloaded_local_file_path, $connections);
957+
958+
$this->assertFalse($response);
959+
$this->assertTrue(unlink($cookie_jar));
960+
}
961+
887962
public function testMaxFilesize()
888963
{
889964
$tests = [

tests/server.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,13 @@
283283
$unsafe_file_path = $_GET['file_path'];
284284
header('Content-Type: image/png');
285285
header('Content-Disposition: attachment; filename="image.png"');
286-
header('Content-Length: ' . filesize($unsafe_file_path));
287-
header('ETag: ' . md5_file($unsafe_file_path));
288-
readfile($unsafe_file_path);
286+
287+
if (!isset($_SERVER['HTTP_RANGE'])) {
288+
header('ETag: ' . md5_file($unsafe_file_path));
289+
}
290+
291+
$server = new ContentRangeServer\ContentRangeServer();
292+
$server->serve($unsafe_file_path);
289293
exit;
290294
} elseif ($test === 'download_file_size') {
291295
if (isset($_GET['http_response_code'])) {

0 commit comments

Comments
 (0)