-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Implementation of XSendFile for nginx #8
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. streamName = basename($filename); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. method called setStreamName will be used to store filename in XSendFileStreamResponseSender. with basename() not existent file /var/www/download/downloaded.tar.gz There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, so no basename here. path1: /protected/ use default: path1 and filename, e.g.
As we can't know what nginx maps for the path, we can not do any file_exists checks on them, and that's okay. |
||
|
||
$headers = new Headers(); | ||
$headers->addHeaders( | ||
array( | ||
'Content-Disposition' => 'attachment; filename="' . $filename . '"', | ||
'Content-Type' => 'application/octet-stream', | ||
) | ||
); | ||
$response->setHeaders($headers); | ||
return $response; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
/* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
* | ||
* This software consists of voluntary contributions made by many individuals | ||
* and is licensed under the MIT license. | ||
*/ | ||
|
||
namespace HumusStreamResponseSender\Exception; | ||
|
||
/** | ||
* @category Humus | ||
* @package HumusStreamResponseSender | ||
* @license MIT | ||
*/ | ||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,7 +44,7 @@ public function onBootstrap(EventInterface $e) | |
{ | ||
$app = $e->getTarget(); | ||
$serviceManager = $app->getServiceManager(); | ||
$streamResponseSender = $serviceManager->get(__NAMESPACE__ . '\StreamResponseSender'); | ||
$streamResponseSender = $serviceManager->get(__NAMESPACE__ . '\XSendFileStreamResponseSender'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again - use the factory, see above. |
||
$sharedEventManager = $app->getEventManager()->getSharedManager(); | ||
/* @var $sharedEventManager SharedEventManager */ | ||
$sharedEventManager->attach( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert this change, this is made special this way. |
||
{ | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert this change, this is made special this way. |
||
{ | ||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<?php | ||
/* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
* | ||
* This software consists of voluntary contributions made by many individuals | ||
* and is licensed under the MIT license. | ||
*/ | ||
|
||
namespace HumusStreamResponseSender; | ||
|
||
use Traversable; | ||
use Zend\Stdlib\AbstractOptions; | ||
use HumusStreamResponseSender\XSendFileServerOptions; | ||
|
||
/** | ||
* @category Humus | ||
* @package HumusStreamResponseSender | ||
* @license MIT | ||
*/ | ||
class XSendFileOptions extends AbstractOptions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use the other options class for this. You can use seperated interfaces. |
||
{ | ||
/** | ||
* @var string | ||
*/ | ||
protected $server; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
protected $internalLocation; | ||
|
||
protected $queryParameter; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
protected $serverOptions; | ||
|
||
/** | ||
* @param $serverOptions | ||
* @return $this | ||
*/ | ||
public function setServerOptions($serverOptions) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't use array here, specify all server options instead, e.g. |
||
{ | ||
$this->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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put this in factory.