Skip to content

Commit 54be433

Browse files
authored
Merge pull request php-curl-class#607 from zachborboa/602
Fix lingering temporary download file when using MultiCurl::addDownload
2 parents fe1a65b + 2d591c3 commit 54be433

File tree

3 files changed

+182
-13
lines changed

3 files changed

+182
-13
lines changed

src/Curl/Curl.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ public function delete($url, $query_parameters = array(), $data = array())
290290
*/
291291
public function download($url, $mixed_filename)
292292
{
293+
// Use tmpfile() or php://temp to avoid "Too many open files" error.
293294
if (is_callable($mixed_filename)) {
294295
$this->downloadCompleteCallback = $mixed_filename;
295296
$this->downloadFileName = null;
@@ -302,17 +303,17 @@ public function download($url, $mixed_filename)
302303
// path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid,
303304
// but unsatisfiable.
304305
$download_filename = $filename . '.pccdownload';
306+
$this->downloadFileName = $download_filename;
305307

306-
$mode = 'wb';
307308
// Attempt to resume download only when a temporary download file exists and is not empty.
308309
if (is_file($download_filename) && $filesize = filesize($download_filename)) {
309-
$mode = 'ab';
310310
$first_byte_position = $filesize;
311311
$range = $first_byte_position . '-';
312312
$this->setOpt(CURLOPT_RANGE, $range);
313+
$this->fileHandle = fopen($download_filename, 'ab');
314+
} else {
315+
$this->fileHandle = fopen($download_filename, 'wb');
313316
}
314-
$this->downloadFileName = $download_filename;
315-
$this->fileHandle = fopen($download_filename, $mode);
316317

317318
// Move the downloaded temporary file to the destination save path.
318319
$this->downloadCompleteCallback = function ($instance, $fh) use ($download_filename, $filename) {

src/Curl/MultiCurl.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,30 @@ public function addDownload($url, $mixed_filename)
9696
// path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid,
9797
// but unsatisfiable.
9898
$download_filename = $filename . '.pccdownload';
99+
$this->downloadFileName = $download_filename;
99100

100-
$mode = 'wb';
101101
// Attempt to resume download only when a temporary download file exists and is not empty.
102102
if (is_file($download_filename) && $filesize = filesize($download_filename)) {
103-
$mode = 'ab';
104103
$first_byte_position = $filesize;
105104
$range = $first_byte_position . '-';
106105
$curl->setOpt(CURLOPT_RANGE, $range);
107-
}
108-
$curl->downloadFileName = $download_filename;
109-
$curl->fileHandle = fopen('php://temp', 'wb');
106+
$curl->fileHandle = fopen($download_filename, 'ab');
107+
108+
// Move the downloaded temporary file to the destination save path.
109+
$curl->downloadCompleteCallback = function ($instance, $fh) use ($download_filename, $filename) {
110+
// Close the open file handle before renaming the file.
111+
if (is_resource($fh)) {
112+
fclose($fh);
113+
}
110114

111-
// Move the downloaded temporary file to the destination save path.
112-
$curl->downloadCompleteCallback = function ($instance, $fh) use ($download_filename) {
113-
file_put_contents($download_filename, stream_get_contents($fh));
114-
};
115+
rename($download_filename, $filename);
116+
};
117+
} else {
118+
$curl->fileHandle = fopen('php://temp', 'wb');
119+
$curl->downloadCompleteCallback = function ($instance, $fh) use ($filename) {
120+
file_put_contents($filename, stream_get_contents($fh));
121+
};
122+
}
115123
}
116124

117125
$curl->setOpt(CURLOPT_FILE, $curl->fileHandle);

tests/PHPCurlClass/PHPMultiCurlClassTest.php

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2561,6 +2561,166 @@ public function testXMLDecoder()
25612561
$multi_curl->start();
25622562
}
25632563

2564+
public function testDownload()
2565+
{
2566+
// Create and upload a file.
2567+
$upload_file_path = \Helper\get_png();
2568+
$uploaded_file_path = \Helper\upload_file_to_server($upload_file_path);
2569+
2570+
// Download the file.
2571+
$downloaded_file_path = tempnam('/tmp', 'php-curl-class.');
2572+
$multi_curl = new MultiCurl();
2573+
$multi_curl->setHeader('X-DEBUG-TEST', 'download_response');
2574+
$multi_curl->addDownload(Test::TEST_URL . '?' . http_build_query(array(
2575+
'file_path' => $uploaded_file_path,
2576+
)), $downloaded_file_path);
2577+
$multi_curl->complete(function ($instance) use ($upload_file_path) {
2578+
\PHPUnit\Framework\Assert::assertFalse($instance->error);
2579+
\PHPUnit\Framework\Assert::assertEquals(md5_file($upload_file_path), $instance->responseHeaders['ETag']);
2580+
});
2581+
$multi_curl->start();
2582+
$this->assertNotEquals($uploaded_file_path, $downloaded_file_path);
2583+
2584+
$this->assertEquals(filesize($upload_file_path), filesize($downloaded_file_path));
2585+
$this->assertEquals(md5_file($upload_file_path), md5_file($downloaded_file_path));
2586+
2587+
// Remove server file.
2588+
\Helper\remove_file_from_server($uploaded_file_path);
2589+
2590+
unlink($upload_file_path);
2591+
unlink($downloaded_file_path);
2592+
$this->assertFalse(file_exists($upload_file_path));
2593+
$this->assertFalse(file_exists($downloaded_file_path));
2594+
}
2595+
2596+
public function testDownloadRange()
2597+
{
2598+
// Create and upload a file.
2599+
$filename = \Helper\get_png();
2600+
$uploaded_file_path = \Helper\upload_file_to_server($filename);
2601+
2602+
$filesize = filesize($filename);
2603+
2604+
foreach (array(
2605+
false,
2606+
0,
2607+
1,
2608+
2,
2609+
3,
2610+
5,
2611+
10,
2612+
25,
2613+
50,
2614+
$filesize - 3,
2615+
$filesize - 2,
2616+
$filesize - 1,
2617+
2618+
// A partial temporary file having the exact same file size as the complete source file should only
2619+
// occur under certain circumstances (almost never). When the download successfully completed, the
2620+
// temporary file should have been moved to the download destination save path. However, it is possible
2621+
// that a larger file download was interrupted after which the source file was updated and now has the
2622+
// exact same file size as the partial temporary. When resuming the download, the range is now
2623+
// unsatisfiable as the first byte position exceeds the available range. The entire file should be
2624+
// downloaded again.
2625+
$filesize - 0,
2626+
2627+
// A partial temporary file having a larger file size than the complete source file should only occur
2628+
// under certain circumstances. This is possible when a download was interrupted after which the source
2629+
// file was updated with a smaller file. When resuming the download, the range is now unsatisfiable as
2630+
// the first byte position exceeds the the available range. The entire file should be downloaded again.
2631+
$filesize + 1,
2632+
$filesize + 2,
2633+
$filesize + 3,
2634+
2635+
) as $length) {
2636+
$source = Test::TEST_URL;
2637+
$destination = \Helper\get_tmp_file_path();
2638+
2639+
// Start with no file.
2640+
if ($length === false) {
2641+
$this->assertFalse(file_exists($destination));
2642+
2643+
// Start with $length bytes of file.
2644+
} else {
2645+
// Simulate resuming partially downloaded temporary file.
2646+
$partial_filename = $destination . '.pccdownload';
2647+
2648+
if ($length === 0) {
2649+
$partial_content = '';
2650+
} else {
2651+
$file = fopen($filename, 'rb');
2652+
$partial_content = fread($file, $length);
2653+
fclose($file);
2654+
}
2655+
2656+
// Partial content size should be $length bytes large for testing resume download behavior.
2657+
if ($length <= $filesize) {
2658+
$this->assertEquals($length, strlen($partial_content));
2659+
2660+
// Partial content should not be larger than the original file size.
2661+
} else {
2662+
$this->assertEquals($filesize, strlen($partial_content));
2663+
}
2664+
2665+
file_put_contents($partial_filename, $partial_content);
2666+
$this->assertEquals(strlen($partial_content), strlen(file_get_contents($partial_filename)));
2667+
}
2668+
2669+
// Download (the remaining bytes of) the file.
2670+
$multi_curl = new MultiCurl();
2671+
$multi_curl->setHeader('X-DEBUG-TEST', 'download_file_range');
2672+
$multi_curl->addDownload($source . '?' . http_build_query(array(
2673+
'file_path' => $uploaded_file_path,
2674+
)), $destination);
2675+
2676+
clearstatcache();
2677+
2678+
$instance_error = false;
2679+
$multi_curl->complete(function ($instance) use ($filesize, $length, $destination, &$instance_error) {
2680+
$expected_bytes_downloaded = $filesize - min($length, $filesize);
2681+
$bytes_downloaded = $instance->responseHeaders['content-length'];
2682+
if ($length === false || $length === 0) {
2683+
$expected_http_status_code = 200; // 200 OK
2684+
\PHPUnit\Framework\Assert::assertEquals($expected_bytes_downloaded, $bytes_downloaded);
2685+
} elseif ($length >= $filesize) {
2686+
$expected_http_status_code = 416; // 416 Requested Range Not Satisfiable
2687+
} else {
2688+
$expected_http_status_code = 206; // 206 Partial Content
2689+
\PHPUnit\Framework\Assert::assertEquals($expected_bytes_downloaded, $bytes_downloaded);
2690+
}
2691+
\PHPUnit\Framework\Assert::assertEquals($expected_http_status_code, $instance->httpStatusCode);
2692+
$instance_error = $instance->error;
2693+
});
2694+
$multi_curl->start();
2695+
2696+
if (!$instance_error) {
2697+
$this->assertEquals($filesize, filesize($destination));
2698+
unlink($destination);
2699+
$this->assertFalse(file_exists($destination));
2700+
}
2701+
}
2702+
2703+
// Remove server file.
2704+
\Helper\remove_file_from_server($uploaded_file_path);
2705+
2706+
unlink($filename);
2707+
$this->assertFalse(file_exists($filename));
2708+
}
2709+
2710+
public function testDownloadErrorDeleteTemporaryFile()
2711+
{
2712+
$destination = \Helper\get_tmp_file_path();
2713+
2714+
$multi_curl = new MultiCurl();
2715+
$multi_curl->setHeader('X-DEBUG-TEST', '404');
2716+
$multi_curl->addDownload(Test::TEST_URL, $destination);
2717+
$multi_curl->complete(function ($instance) use ($destination) {
2718+
\PHPUnit\Framework\Assert::assertFalse(file_exists($instance->getDownloadFileName()));
2719+
\PHPUnit\Framework\Assert::assertFalse(file_exists($destination));
2720+
});
2721+
$multi_curl->start();
2722+
}
2723+
25642724
public function testDownloadCallback()
25652725
{
25662726
// Upload a file.

0 commit comments

Comments
 (0)