mirror of
https://github.com/php-curl-class/php-curl-class.git
synced 2026-08-26 16:29:19 +00:00
Merge pull request #410 from zachborboa/master
Support resume for Curl::download()
This commit is contained in:
+21
-1
@@ -305,7 +305,27 @@ class Curl
|
||||
$fh = tmpfile();
|
||||
} else {
|
||||
$filename = $mixed_filename;
|
||||
$fh = fopen($filename, 'wb');
|
||||
|
||||
// Use a temporary file when downloading. Not using a temporary file can cause an error when an existing
|
||||
// file has already fully completed downloading and a new download is started with the same destination save
|
||||
// path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid,
|
||||
// but unsatisfiable.
|
||||
$download_filename = $filename . '.pccdownload';
|
||||
|
||||
$mode = 'wb';
|
||||
// Attempt to resume download only when a temporary download file exists and is not empty.
|
||||
if (file_exists($download_filename) && $filesize = filesize($download_filename)) {
|
||||
$mode = 'ab';
|
||||
$first_byte_position = $filesize;
|
||||
$range = $first_byte_position . '-';
|
||||
$this->setOpt(CURLOPT_RANGE, $range);
|
||||
}
|
||||
$fh = fopen($download_filename, $mode);
|
||||
|
||||
// Move the downloaded temporary file to the destination save path.
|
||||
$this->downloadCompleteFunction = function ($fh) use ($download_filename, $filename) {
|
||||
rename($download_filename, $filename);
|
||||
};
|
||||
}
|
||||
|
||||
$this->setOpt(CURLOPT_FILE, $fh);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace ContentRangeServer;
|
||||
|
||||
use RangeHeader\RangeHeader;
|
||||
|
||||
class ContentRangeServer
|
||||
{
|
||||
public function serve($path)
|
||||
{
|
||||
$range = new RangeHeader($_SERVER['HTTP_RANGE']);
|
||||
|
||||
$filesize = filesize($path);
|
||||
$fp = fopen($path, 'r');
|
||||
|
||||
if (!isset($_SERVER['HTTP_RANGE'])) {
|
||||
header('HTTP/1.1 200 OK');
|
||||
header('Content-Length: ' . $filesize);
|
||||
header('Accept-Ranges: bytes');
|
||||
fpassthru($fp);
|
||||
} else {
|
||||
header('HTTP/1.1 206 Partial Content');
|
||||
header('Content-Length: ' . $range->getLength($filesize));
|
||||
header('Content-Range: ' . $range->getContentRangeHeader($filesize));
|
||||
|
||||
$start = $range->getFirstBytePosition($filesize);
|
||||
if ($start > 0) {
|
||||
fseek($fp, $start, SEEK_SET);
|
||||
}
|
||||
|
||||
$length = $range->getLength($filesize);
|
||||
$chunk_size = 4096;
|
||||
while ($length) {
|
||||
$read = $length > $chunk_size ? $chunk_size : $length;
|
||||
$length -= $read;
|
||||
echo fread($fp, $read);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
use Curl\Curl;
|
||||
@@ -67,6 +68,15 @@ function create_tmp_file($data)
|
||||
return $tmp_file;
|
||||
}
|
||||
|
||||
function get_tmp_file_path()
|
||||
{
|
||||
// Return temporary file path without creating file.
|
||||
$tmp_file_path =
|
||||
rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) .
|
||||
DIRECTORY_SEPARATOR . 'php-curl-class.' . uniqid(rand(), true);
|
||||
return $tmp_file_path;
|
||||
}
|
||||
|
||||
function get_png()
|
||||
{
|
||||
$tmp_filename = tempnam('/tmp', 'php-curl-class.');
|
||||
@@ -89,3 +99,29 @@ if (function_exists('finfo_open')) {
|
||||
return $mime_type;
|
||||
}
|
||||
}
|
||||
|
||||
function upload_file_to_server($upload_file_path) {
|
||||
$upload_test = new Test();
|
||||
$upload_test->server('upload_response', 'POST', array(
|
||||
'image' => '@' . $upload_file_path,
|
||||
));
|
||||
$uploaded_file_path = $upload_test->curl->response->file_path;
|
||||
|
||||
// Ensure files are not the same path.
|
||||
assert(!($upload_file_path === $uploaded_file_path));
|
||||
|
||||
// Ensure file uploaded successfully.
|
||||
assert(md5_file($upload_file_path) === $upload_test->curl->responseHeaders['ETag']);
|
||||
|
||||
return $uploaded_file_path;
|
||||
}
|
||||
|
||||
function remove_file_from_server($uploaded_file_path) {
|
||||
$download_test = new Test();
|
||||
|
||||
// Ensure file successfully removed.
|
||||
assert('true' === $download_test->server('upload_cleanup', 'POST', array(
|
||||
'file_path' => $uploaded_file_path,
|
||||
)));
|
||||
assert(file_exists($uploaded_file_path) === false);
|
||||
}
|
||||
|
||||
@@ -609,15 +609,9 @@ class CurlTest extends PHPUnit_Framework_TestCase
|
||||
|
||||
public function testDownload()
|
||||
{
|
||||
// Upload a file.
|
||||
// Create and upload a file.
|
||||
$upload_file_path = Helper\get_png();
|
||||
$upload_test = new Test();
|
||||
$upload_test->server('upload_response', 'POST', array(
|
||||
'image' => '@' . $upload_file_path,
|
||||
));
|
||||
$uploaded_file_path = $upload_test->curl->response->file_path;
|
||||
$this->assertNotEquals($upload_file_path, $uploaded_file_path);
|
||||
$this->assertEquals(md5_file($upload_file_path), $upload_test->curl->responseHeaders['ETag']);
|
||||
$uploaded_file_path = Helper\upload_file_to_server($upload_file_path);
|
||||
|
||||
// Download the file.
|
||||
$downloaded_file_path = tempnam('/tmp', 'php-curl-class.');
|
||||
@@ -638,27 +632,19 @@ class CurlTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertFalse(is_bool($download_test->curl->rawResponse));
|
||||
|
||||
// Remove server file.
|
||||
$download_test = new Test();
|
||||
$this->assertEquals('true', $download_test->server('upload_cleanup', 'POST', array(
|
||||
'file_path' => $uploaded_file_path,
|
||||
)));
|
||||
Helper\remove_file_from_server($uploaded_file_path);
|
||||
|
||||
unlink($upload_file_path);
|
||||
unlink($downloaded_file_path);
|
||||
$this->assertFalse(file_exists($upload_file_path));
|
||||
$this->assertFalse(file_exists($uploaded_file_path));
|
||||
$this->assertFalse(file_exists($downloaded_file_path));
|
||||
}
|
||||
|
||||
public function testDownloadCallback()
|
||||
{
|
||||
// Upload a file.
|
||||
// Create and upload a file.
|
||||
$upload_file_path = Helper\get_png();
|
||||
$upload_test = new Test();
|
||||
$upload_test->server('upload_response', 'POST', array(
|
||||
'image' => '@' . $upload_file_path,
|
||||
));
|
||||
$uploaded_file_path = $upload_test->curl->response->file_path;
|
||||
$uploaded_file_path = Helper\upload_file_to_server($upload_file_path);
|
||||
|
||||
// Download the file.
|
||||
$callback_called = false;
|
||||
@@ -679,13 +665,101 @@ class CurlTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertTrue($callback_called);
|
||||
|
||||
// Remove server file.
|
||||
$this->assertEquals('true', $upload_test->server('upload_cleanup', 'POST', array(
|
||||
'file_path' => $uploaded_file_path,
|
||||
)));
|
||||
Helper\remove_file_from_server($uploaded_file_path);
|
||||
|
||||
unlink($upload_file_path);
|
||||
$this->assertFalse(file_exists($upload_file_path));
|
||||
$this->assertFalse(file_exists($uploaded_file_path));
|
||||
}
|
||||
|
||||
public function testDownloadRange()
|
||||
{
|
||||
// Create and upload a file.
|
||||
$filename = Helper\get_png();
|
||||
$uploaded_file_path = Helper\upload_file_to_server($filename);
|
||||
|
||||
$filesize = filesize($filename);
|
||||
|
||||
foreach (array(
|
||||
false,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
10,
|
||||
25,
|
||||
50,
|
||||
$filesize - 3,
|
||||
$filesize - 2,
|
||||
$filesize - 1,
|
||||
) as $length) {
|
||||
|
||||
$source = Test::TEST_URL;
|
||||
$destination = Helper\get_tmp_file_path();
|
||||
|
||||
// Start with no file.
|
||||
if ($length === false) {
|
||||
$this->assertFalse(file_exists($destination));
|
||||
|
||||
// Start with $length bytes of file.
|
||||
} else {
|
||||
|
||||
// Simulate resuming partially downloaded temporary file.
|
||||
$partial_filename = $destination . '.pccdownload';
|
||||
|
||||
if ($length === 0) {
|
||||
$partial_content = '';
|
||||
} else {
|
||||
$file = fopen($filename, 'rb');
|
||||
$partial_content = fread($file, $length);
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
// Partial content size should be $length bytes large for testing resume download behavior.
|
||||
if ($length <= $filesize) {
|
||||
$this->assertEquals($length, strlen($partial_content));
|
||||
|
||||
// Partial content should not be larger than the original file size.
|
||||
} else {
|
||||
$this->assertEquals($filesize, strlen($partial_content));
|
||||
}
|
||||
|
||||
file_put_contents($partial_filename, $partial_content);
|
||||
$this->assertEquals(strlen($partial_content), strlen(file_get_contents($partial_filename)));
|
||||
}
|
||||
|
||||
// Download (the remaining bytes of) the file.
|
||||
$curl = new Curl();
|
||||
$curl->setHeader('X-DEBUG-TEST', 'download_file_range');
|
||||
$curl->download($source . '?' . http_build_query(array(
|
||||
'file_path' => $uploaded_file_path,
|
||||
)), $destination);
|
||||
|
||||
clearstatcache();
|
||||
|
||||
$expected_bytes_downloaded = $filesize - min($length, $filesize);
|
||||
$bytes_downloaded = $curl->responseHeaders['content-length'];
|
||||
if ($length === false || $length === 0) {
|
||||
$expected_http_status_code = 200; // 200 OK
|
||||
$this->assertEquals($expected_bytes_downloaded, $bytes_downloaded);
|
||||
} elseif ($length >= $filesize) {
|
||||
$expected_http_status_code = 416; // 416 Requested Range Not Satisfiable
|
||||
} else {
|
||||
$expected_http_status_code = 206; // 206 Partial Content
|
||||
$this->assertEquals($expected_bytes_downloaded, $bytes_downloaded);
|
||||
}
|
||||
$this->assertEquals($expected_http_status_code, $curl->httpStatusCode);
|
||||
$this->assertEquals($filesize, filesize($destination));
|
||||
|
||||
unlink($destination);
|
||||
$this->assertFalse(file_exists($destination));
|
||||
}
|
||||
|
||||
// Remove server file.
|
||||
Helper\remove_file_from_server($uploaded_file_path);
|
||||
|
||||
unlink($filename);
|
||||
$this->assertFalse(file_exists($filename));
|
||||
}
|
||||
|
||||
public function testMaxFilesize()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace RangeHeader;
|
||||
|
||||
class RangeHeader
|
||||
{
|
||||
private $first_byte;
|
||||
private $last_byte;
|
||||
|
||||
public function __construct($http_range_header)
|
||||
{
|
||||
// Simulate basic support for the Content-Range header.
|
||||
preg_match('/bytes=(\d+)?-(\d+)?/', $http_range_header, $matches);
|
||||
$this->first_byte = isset($matches['1']) ? (int)$matches['1'] : null;
|
||||
$this->last_byte = isset($matches['2']) ? (int)$matches['2'] : null;
|
||||
}
|
||||
|
||||
public function getFirstBytePosition($file_size)
|
||||
{
|
||||
$size = (int)$file_size;
|
||||
|
||||
if ($this->first_byte === null) {
|
||||
return $size - 1 - $this->last_byte;
|
||||
}
|
||||
|
||||
return $this->first_byte;
|
||||
}
|
||||
|
||||
public function getLastBytePosition($file_size)
|
||||
{
|
||||
$size = (int)$file_size;
|
||||
|
||||
if ($this->last_byte === null) {
|
||||
return $size - 1;
|
||||
}
|
||||
|
||||
return $this->last_byte;
|
||||
}
|
||||
|
||||
public function getLength($file_size)
|
||||
{
|
||||
$size = (int)$file_size;
|
||||
|
||||
return $this->getLastBytePosition($size) - $this->getFirstBytePosition($size) + 1;
|
||||
}
|
||||
|
||||
public function getContentRangeHeader($file_size)
|
||||
{
|
||||
return
|
||||
'bytes ' . $this->getFirstBytePosition($file_size) . '-' . $this->getLastBytePosition($file_size) . '/' .
|
||||
$file_size;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once 'ContentRangeServer.php';
|
||||
require_once 'RangeHeader.php';
|
||||
require_once 'Helper.php';
|
||||
|
||||
use \Helper\Test;
|
||||
@@ -257,6 +260,11 @@ if ($test === 'http_basic_auth') {
|
||||
header('ETag: ' . md5($str));
|
||||
echo $str;
|
||||
exit;
|
||||
} elseif ($test === 'download_file_range') {
|
||||
$unsafe_file_path = $_GET['file_path'];
|
||||
$server = new ContentRangeServer\ContentRangeServer();
|
||||
$server->serve($unsafe_file_path);
|
||||
exit;
|
||||
} elseif ($test === 'timeout') {
|
||||
$unsafe_seconds = $_GET['seconds'];
|
||||
$start = time();
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
set -x
|
||||
php -S 127.0.0.1:8000 -t PHPCurlClass/ &> /dev/null &
|
||||
pid="${!}"
|
||||
extra_args="${@}"
|
||||
phpunit \
|
||||
--configuration phpunit.xml \
|
||||
--debug \
|
||||
--verbose
|
||||
--verbose \
|
||||
${extra_args}
|
||||
kill "${pid}"
|
||||
|
||||
Reference in New Issue
Block a user