diff --git a/config/module.config.php b/config/module.config.php index 590da30..91e162a 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -8,7 +8,8 @@ ), 'service_manager' => array( 'factories' => array( - 'HumusStreamResponseSender\StreamResponseSender' => 'HumusStreamResponseSender\StreamResponseSenderFactory' + 'HumusStreamResponseSender\XSendFileStreamResponseSender' + => 'HumusStreamResponseSender\StreamResponseSenderFactory' ) ) -); \ No newline at end of file +); diff --git a/src/HumusStreamResponseSender/Controller/Plugin/Stream.php b/src/HumusStreamResponseSender/Controller/Plugin/Stream.php index 0f9b277..63ae9e8 100644 --- a/src/HumusStreamResponseSender/Controller/Plugin/Stream.php +++ b/src/HumusStreamResponseSender/Controller/Plugin/Stream.php @@ -30,7 +30,6 @@ */ class Stream extends AbstractPlugin { - /** * Returns a stream response for a binary file download * @@ -47,12 +46,12 @@ class Stream extends AbstractPlugin * * @param string $filename * @return StreamResponse - * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException */ public function binaryFile($filename) { if (!file_exists($filename) || !is_readable($filename)) { - throw new Exception\RuntimeException( + throw new Exception\InvalidArgumentException( 'Invalid filename given; not readable or does not exist' ); } @@ -62,7 +61,6 @@ public function binaryFile($filename) $response = new StreamResponse(); $response->setStream($resource); - $response->setStatusCode(206); $response->setStreamName($basename); $response->setContentLength(filesize($filename)); @@ -77,4 +75,21 @@ public function binaryFile($filename) $response->setHeaders($headers); return $response; } + + public function xSendFile($filename) + { + $response = new StreamResponse(); + + $response->setStreamName('X-SendFile'); + + $headers = new Headers(); + $headers->addHeaders( + array( + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Type' => 'application/octet-stream', + ) + ); + $response->setHeaders($headers); + return $response; + } } diff --git a/src/HumusStreamResponseSender/Exception/InvalidArgumentException.php b/src/HumusStreamResponseSender/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..177db6d --- /dev/null +++ b/src/HumusStreamResponseSender/Exception/InvalidArgumentException.php @@ -0,0 +1,28 @@ +getTarget(); $serviceManager = $app->getServiceManager(); - $streamResponseSender = $serviceManager->get(__NAMESPACE__ . '\StreamResponseSender'); + $streamResponseSender = $serviceManager->get(__NAMESPACE__ . '\XSendFileStreamResponseSender'); $sharedEventManager = $app->getEventManager()->getSharedManager(); /* @var $sharedEventManager SharedEventManager */ $sharedEventManager->attach( diff --git a/src/HumusStreamResponseSender/StreamResponseSender.php b/src/HumusStreamResponseSender/StreamResponseSender.php index e74e356..dc27ade 100644 --- a/src/HumusStreamResponseSender/StreamResponseSender.php +++ b/src/HumusStreamResponseSender/StreamResponseSender.php @@ -48,12 +48,17 @@ class StreamResponseSender extends SimpleStreamResponseSender /** * @var int */ - private $range; + private $rangeStart; /** - * @param array|Traversable|null|Options $options + * @var int + */ + private $rangeEnd; + + /** + * @param null|Options $options */ - public function __construct($options = null) + public function __construct(Options $options = null) { if (null !== $options) { $this->setOptions($options); @@ -63,13 +68,11 @@ public function __construct($options = null) /** * Set options * - * @param array|Traversable|Options $options + * @param Options $options + * @return $this */ - public function setOptions($options) + public function setOptions(Options $options) { - if (!$options instanceof Options) { - $options = new Options($options); - } $this->options = $options; return $this; } @@ -81,9 +84,6 @@ public function setOptions($options) */ public function getOptions() { - if (!$this->options instanceof Options) { - $this->options = new Options(); - } return $this->options; } @@ -123,31 +123,61 @@ public function sendHeaders(SendResponseEvent $event) $responseHeaders->addHeaderLine('Content-Transfer-Encoding', 'binary'); } - $responseHeaders->addHeaderLine('Accept-Ranges', 'bytes'); - $size = $response->getContentLength(); $size2 = $size - 1; - $requestHeaders = $this->getRequest()->getHeaders(); - $length = $size; - $range = '0-'; - $this->range = 0; + $this->rangeStart = 0; + $this->rangeEnd = null; - if ($requestHeaders->has('Range')) { + $enableDownloadResume = $this->getOptions()->getEnableDownloadResume(); + $requestHeaders = $this->getRequest()->getHeaders(); + + if ($enableDownloadResume && $requestHeaders->has('Range')) { list($a, $range) = explode('=', $requestHeaders->get('Range')->getFieldValue()); - str_replace($range, "-", $range); + if (substr($range, -1) == '-') { + // range: 3442- + $range = substr($range, 0, -1); + if (!is_numeric($range) || $range > $size2) { + // 416 (Requested range not satisfiable) + $response->setStatusCode(416); + $event->setContentSent(); + return $this; + } + $this->rangeStart = $range; $length = $size - $range; + } else { + $ranges = explode('-', $range, 2); + $rangeStart = $ranges[0]; + $rangeEnd = $ranges[1]; + if (!is_numeric($rangeStart) + || !is_numeric($rangeEnd) + || ($rangeStart >= $rangeEnd) + || $rangeEnd > $size2 + ) { + // 416 (Requested range not satisfiable) + $response->setStatusCode(416); + $event->setContentSent(); + return $this; + } + $this->rangeStart = $rangeStart; + $this->rangeEnd = $rangeEnd; + $length = $rangeEnd - $rangeStart; + $size2 = $rangeEnd; + } $response->setStatusCode(206); // 206 (Partial Content) - $this->range = (int) $range; } + $responseHeaders->addHeaderLine('Content-Length: ' . $length); + + if ($enableDownloadResume) { $responseHeaders->addHeaders( array( - 'Content-Length: ' . $length, - 'Content-Range: bytes ' . $range . $size2 . '/' . $size, + 'Accept-Ranges: bytes', + 'Content-Range: bytes ' . $this->rangeStart . $size2 . '/' . $size, ) ); + } parent::sendHeaders($event); } @@ -179,23 +209,37 @@ public function sendStream(SendResponseEvent $event) } set_time_limit(0); + $rangeStart = $this->rangeStart; + if (null !== $this->rangeEnd) { + $rangeEnd = $this->rangeEnd; + $length = $rangeEnd-$rangeStart; + } else { + $length = $response->getContentLength(); + } - fseek($stream, $this->range); - + fseek($stream, $rangeStart); $chunkSize = $options->getChunkSize(); + if ($chunkSize > $length) { + $chunkSize = $length; + } + $sizeSent = 0; + while (!feof($stream) && (connection_status()==0)) { echo fread($stream, $chunkSize); flush(); - ob_flush(); + + $sizeSent += $chunkSize; + + if ($sizeSent == $length) { + $event->setContentSent(); + return $this; + } if ($enableSpeedLimit) { sleep(1); } } - - $event->setContentSent(); - return $this; } } diff --git a/src/HumusStreamResponseSender/StreamResponseSenderFactory.php b/src/HumusStreamResponseSender/StreamResponseSenderFactory.php index d1a332e..e9ecdf3 100644 --- a/src/HumusStreamResponseSender/StreamResponseSenderFactory.php +++ b/src/HumusStreamResponseSender/StreamResponseSenderFactory.php @@ -42,7 +42,15 @@ public function createService(ServiceLocatorInterface $serviceLocator) } else { $options = null; } - $streamResponseSender = new StreamResponseSender($options); + + if ($options['is_x_send_file']) { + $options = new XSendFileOptions($options['x_send_file']); + $streamResponseSender = new XSendFileStreamResponseSender($options); + } else { + $options = new Options($options['default']); + $streamResponseSender = new StreamResponseSender($options); + } + $streamResponseSender->setRequest($serviceLocator->get('Request')); return $streamResponseSender; } diff --git a/src/HumusStreamResponseSender/XSendFileOptions.php b/src/HumusStreamResponseSender/XSendFileOptions.php new file mode 100644 index 0000000..61964c1 --- /dev/null +++ b/src/HumusStreamResponseSender/XSendFileOptions.php @@ -0,0 +1,103 @@ +serverOptions = $serverOptions; + return $this; + } + + /** + * @return array + */ + public function getServerOptions() + { + return $this->serverOptions; + } + + /** + * @param mixed $queryParameter + * @return $this + */ + public function setQueryParameter($queryParameter) + { + $this->queryParameter = $queryParameter; + return $this; + } + + /** + * @return mixed + */ + public function getQueryParameter() + { + return $this->queryParameter; + } + + /** + * @param array $server + * + * @return array + */ + public function setServer($server) + { + $this->server = $server; + return $server; + } + + /** + * @return array + */ + public function getServer() + { + return $this->server; + } +} diff --git a/src/HumusStreamResponseSender/XSendFileServerOptions/NginxOptions.php b/src/HumusStreamResponseSender/XSendFileServerOptions/NginxOptions.php new file mode 100644 index 0000000..fd4a679 --- /dev/null +++ b/src/HumusStreamResponseSender/XSendFileServerOptions/NginxOptions.php @@ -0,0 +1,161 @@ +buffering = $buffering; + } + return $this; + } + + /** + * @return boolean + */ + public function getBuffering() + { + return $this->buffering; + } + + /** + * @param string $charset + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * @param bool|int $nginxInternalCacheExpires + * + * @return $this + * @throws \HumusStreamResponseSender\Exception\InvalidArgumentException + */ + public function setNginxInternalCacheExpires($nginxInternalCacheExpires) + { + if (false === $nginxInternalCacheExpires + || is_numeric($nginxInternalCacheExpires) && $nginxInternalCacheExpires > 0 + ) { + $this->nginxInternalCacheExpires = $nginxInternalCacheExpires; + return $this; + } + + throw new InvalidArgumentException('Invalid value for nginx internal cache expires, can be numer of secounds or false'); + } + + /** + * @return bool|int + */ + public function getNginxInternalCacheExpires() + { + return $this->nginxInternalCacheExpires; + } + + /** + * @param bool|int $rateLimit + * + * @throws InvalidArgumentException + * @return $this + */ + public function setRateLimit($rateLimit) + { + if (false === $rateLimit || is_numeric($rateLimit) && $rateLimit > 0) { + $this->rateLimit = $rateLimit; + return $this; + } + + throw new InvalidArgumentException('Rate limit could be number of bytes or false'); + } + + /** + * @return bool|int + */ + public function getRateLimit() + { + return $this->rateLimit; + } + + /** + * @param $internalLocation + * + * @return $this + */ + public function setInternalLocation($internalLocation) + { + $this->internalLocation = trim($internalLocation, '/'); + return $this; + } + + /** + * @return array + */ + public function getInternalLocation() + { + return $this->internalLocation; + } +} diff --git a/src/HumusStreamResponseSender/XSendFileStreamResponseSender.php b/src/HumusStreamResponseSender/XSendFileStreamResponseSender.php new file mode 100644 index 0000000..03a74d7 --- /dev/null +++ b/src/HumusStreamResponseSender/XSendFileStreamResponseSender.php @@ -0,0 +1,185 @@ +setOptions($options); + } + } + + /** + * Set options + * + * @param XSendFileOptions $options + * @return $this + */ + public function setOptions(XSendFileOptions $options) + { + $this->options = $options; + return $this; + } + + /** + * Get options + * + * @return Options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Set request + * + * @param Request $request + */ + public function setRequest(Request $request) + { + $this->request = $request; + } + + /** + * Get request + * + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Send HTTP headers + * + * @param SendResponseEvent $event + * + * @throws \RuntimeException + * @return StreamResponseSender + */ + public function sendHeaders(SendResponseEvent $event) + { + + /* @var $response Stream */ + $response = $event->getResponse(); + $request = $this->getRequest(); + $filename = $request->getQuery($this->options->getQueryParameter()); + + $server = $this->options->getServer(); + $serverOptions = $this->options->getServerOptions(); + + if (static::SRV_NGINX === $server) { + $serverOptions = new NginxOptions($serverOptions); + $headers = $this->getNginxHeaders($serverOptions, $filename); + } else { + throw new \RuntimeException('Not yet implemented for server type: '. $server); + } + + if (empty($headers)) { + // we do nothing here + return; + } + + $responseHeaders = $response->getHeaders(); + $responseHeaders->addHeaders($headers); + + parent::sendHeaders($event); + } + + /** + * Send the stream + * + * the script will not handle sending with X send file + * that is the task of the server + * + * @param SendResponseEvent $event + * @return self + */ + public function sendStream(SendResponseEvent $event) + { + if (!$event->contentSent()) { + $event->setContentSent(); + } + return $this; + } + + /** + * @param NginxOptions $serverOptions + * @param string $filename + */ + protected function getNginxHeaders(NginxOptions $serverOptions, $filename) + { + $location = $serverOptions->getInternalLocation(); + + $expires = $serverOptions->getNginxInternalCacheExpires(); + if (false !== $expires) { + $headers['X-Accel-Expires'] = $expires; + } + + $rateLimit = $serverOptions->getRateLimit(); + if (false !== $rateLimit) { + $headers['X-Accel-Limit-Rate'] = $rateLimit; + } + + $charset = $serverOptions->getCharset(); + if (false !== $charset) { + $headers['X-Accel-Charset'] = $charset; + } + + $headers['X-Accel-Buffering'] = $serverOptions->getBuffering(); + $headers['X-Accel-Redirect'] = '/' . $location . '/' . $filename; + + return $headers; + } +} diff --git a/tests/HumusStreamResponseSenderTest/Controller/Plugin/StreamTest.php b/tests/HumusStreamResponseSenderTest/Controller/Plugin/StreamTest.php new file mode 100644 index 0000000..3a9a84d --- /dev/null +++ b/tests/HumusStreamResponseSenderTest/Controller/Plugin/StreamTest.php @@ -0,0 +1,55 @@ +binaryFile('/invalid/path'); + } + + public function testBinaryFile() + { + $utt = new Stream(); + $filename = __DIR__ . '/../../TestAsset/sample-stream-file.txt'; + $filesize = filesize($filename); + $basename = basename($filename); + + + $response = $utt->binaryFile($filename); + $this->assertInstanceOf('Zend\Http\Response\Stream', $response); + $this->assertInternalType('resource', $response->getStream()); + $this->assertSame($basename, $response->getStreamName()); + $this->assertSame($filesize, $response->getContentLength()); + $headers = $response->getHeaders()->toArray(); + $expectedHeaders = array( + 'Content-Disposition' => 'attachment; filename="' . $basename . '"', + 'Content-Type' => 'application/octet-stream', + ); + $this->assertSame($expectedHeaders, $headers); + } +} diff --git a/tests/HumusStreamResponseSenderTest/ModuleTest.php b/tests/HumusStreamResponseSenderTest/ModuleTest.php new file mode 100644 index 0000000..6c93207 --- /dev/null +++ b/tests/HumusStreamResponseSenderTest/ModuleTest.php @@ -0,0 +1,42 @@ +getConfig(); + $this->assertInternalType('array', $config); + } + + public function testGetAutoloaderConfig() + { + $module = new Module(); + $config = $module->getAutoloaderConfig(); + if (!is_array($config) && !($config instanceof Traversable)) { + $this->fail('getAutoloaderConfig expected to return array or Traversable'); + } + } +} diff --git a/tests/HumusStreamResponseSenderTest/OptionsTest.php b/tests/HumusStreamResponseSenderTest/OptionsTest.php index 9f1fb1a..adaadad 100644 --- a/tests/HumusStreamResponseSenderTest/OptionsTest.php +++ b/tests/HumusStreamResponseSenderTest/OptionsTest.php @@ -16,8 +16,9 @@ * and is licensed under the MIT license. */ -namespace HumusStreamResponseSender; +namespace HumusStreamResponseSenderTest; +use HumusStreamResponseSender\Options; use PHPUnit_Framework_TestCase as TestCase; use Zend\Stdlib\AbstractOptions; diff --git a/tests/HumusStreamResponseSenderTest/StreamResponseSenderTest.php b/tests/HumusStreamResponseSenderTest/StreamResponseSenderTest.php index b3b4db2..284d4ae 100644 --- a/tests/HumusStreamResponseSenderTest/StreamResponseSenderTest.php +++ b/tests/HumusStreamResponseSenderTest/StreamResponseSenderTest.php @@ -26,16 +26,36 @@ class StreamResponseSenderTest extends TestCase { /** - * @runInSeparateProcess + * @var \Zend\Mvc\ResponseSender\SendResponseEvent */ - public function testSendHeadersAndStreamInDefaultMode() - { - if (!function_exists('xdebug_get_headers')) { - $this->markTestSkipped('Xdebug extension needed, skipped test'); - } + protected $mockSendResponseEvent; + /** + * @var string + */ + protected $testFile; + + /** + * @var int + */ + protected $fileSize; + + /** + * @var array + */ + protected $headers; + + /** + * @var \Zend\Http\Request + */ + protected $requestMock; + + protected function setUp() + { $testFile = __DIR__ . '/TestAsset/sample-stream-file.txt'; + $this->testFile = $testFile; $fileSize = filesize($testFile); + $this->fileSize = $fileSize; $basename = basename($testFile); $stream = fopen($testFile, 'rb'); @@ -43,6 +63,7 @@ public function testSendHeadersAndStreamInDefaultMode() 'Content-Disposition: attachment; filename="' . $basename . '"', 'Content-Type: application/octet-stream', ); + $this->headers = $headers; $response = new Stream(); $response->setStream($stream); @@ -50,7 +71,6 @@ public function testSendHeadersAndStreamInDefaultMode() $response->setStreamName($basename); $response->getHeaders()->addHeaders($headers); - $mockSendResponseEvent = $this->getMock( 'Zend\Mvc\ResponseSender\SendResponseEvent', array('getResponse') @@ -62,11 +82,28 @@ public function testSendHeadersAndStreamInDefaultMode() $requestMock = $this->getMockForAbstractClass('Zend\Http\Request'); + $this->requestMock = $requestMock; + $this->mockSendResponseEvent = $mockSendResponseEvent; + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamInDefaultMode() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + $responseSender = new StreamResponseSender(); - $responseSender->setRequest($requestMock); + $responseSender->setRequest($this->requestMock); ob_start(); - $responseSender($mockSendResponseEvent); + $responseSender($this->mockSendResponseEvent); $body = ob_get_clean(); $this->assertEquals(file_get_contents($testFile), $body); @@ -75,14 +112,13 @@ public function testSendHeadersAndStreamInDefaultMode() $headers, array( 'Content-Transfer-Encoding: binary', - 'Accept-Ranges: bytes', - 'Content-Range: bytes 0-' . ($fileSize - 1) . '/' . $fileSize, 'Content-Length: ' . $fileSize ) ); $sentHeaders = xdebug_get_headers(); - $diff = array_diff($sentHeaders, $expectedHeaders); + + $diff = array_diff($expectedHeaders, $sentHeaders); if (count($diff)) { $header = array_shift($diff); @@ -90,4 +126,235 @@ public function testSendHeadersAndStreamInDefaultMode() $this->assertEquals(0, count($diff)); } } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithoutRangeHeaders() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true + ) + ); + $responseSender->setRequest($this->requestMock); + + ob_start(); + $responseSender($this->mockSendResponseEvent); + $body = ob_get_clean(); + + $this->assertEquals(file_get_contents($testFile), $body); + + $expectedHeaders = array_merge( + $headers, + array( + 'Content-Transfer-Encoding: binary', + 'Content-Length: ' . $fileSize + ) + ); + + $sentHeaders = xdebug_get_headers(); + + $diff = array_diff($expectedHeaders, $sentHeaders); + + if (count($diff)) { + $header = array_shift($diff); + $this->assertContains('XDEBUG_SESSION', $header); + $this->assertEquals(0, count($diff)); + } + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithoutRangeHeadersAndChunkSize() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true, + 'chunk_size' => 10 + ) + ); + $responseSender->setRequest($this->requestMock); + + ob_start(); + $responseSender($this->mockSendResponseEvent); + $body = ob_get_clean(); + + $this->assertEquals(file_get_contents($testFile), $body); + + $expectedHeaders = array_merge( + $headers, + array( + 'Content-Transfer-Encoding: binary', + 'Content-Length: ' . $fileSize + ) + ); + + $sentHeaders = xdebug_get_headers(); + + $diff = array_diff($expectedHeaders, $sentHeaders); + + if (count($diff)) { + $header = array_shift($diff); + $this->assertContains('XDEBUG_SESSION', $header); + $this->assertEquals(0, count($diff)); + } + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithInvalidRangeStartHeader() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true + ) + ); + + $this->requestMock->getHeaders()->addHeaderLine('Range: bytes=6290368-'); + $responseSender->setRequest($this->requestMock); + + $responseSender($this->mockSendResponseEvent); + + $this->assertSame(416, $this->mockSendResponseEvent->getResponse()->getStatusCode()); + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithInvalidRangeEndHeader() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true + ) + ); + + $this->requestMock->getHeaders()->addHeaderLine('Range: bytes=1-37487329'); + $responseSender->setRequest($this->requestMock); + + $responseSender($this->mockSendResponseEvent); + + $this->assertSame(416, $this->mockSendResponseEvent->getResponse()->getStatusCode()); + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithInvalidRangeStartAndEndHeader() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true + ) + ); + + $this->requestMock->getHeaders()->addHeaderLine('Range: bytes=6290368-37487329'); + $responseSender->setRequest($this->requestMock); + + $responseSender($this->mockSendResponseEvent); + + $this->assertSame(416, $this->mockSendResponseEvent->getResponse()->getStatusCode()); + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithInvalidRangeStartHeader2() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true + ) + ); + + $this->requestMock->getHeaders()->addHeaderLine('Range: bytes=fkjdsfs-'); + $responseSender->setRequest($this->requestMock); + + $responseSender($this->mockSendResponseEvent); + + $this->assertSame(416, $this->mockSendResponseEvent->getResponse()->getStatusCode()); + } + + /** + * @runInSeparateProcess + */ + public function testSendHeadersAndStreamWithEnabledDownloadResumeWithRangeHeader() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('Xdebug extension needed, skipped test'); + } + + $testFile = $this->testFile; + $fileSize = $this->fileSize; + $headers = $this->headers; + + $responseSender = new StreamResponseSender( + array( + 'enable_download_resume' => true, + 'chunk_size' => 10 + ) + ); + + $this->requestMock->getHeaders()->addHeaderLine('Range: bytes=4-10'); + $responseSender->setRequest($this->requestMock); + + ob_start(); + $responseSender($this->mockSendResponseEvent); + $body = ob_get_clean(); + + $this->assertEquals(' is a ', $body); + + $this->assertSame(206, $this->mockSendResponseEvent->getResponse()->getStatusCode()); + } }