From 22053cdc60d8f7663170c373fb48e43c4624e545 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 10 Jun 2013 08:48:30 -0400 Subject: [PATCH] Response::sendStream WIP --- framework/yii/web/Response.php | 136 ++++++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 30 deletions(-) diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index 12eb2eb..9d57560 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -352,6 +352,95 @@ class Response extends \yii\base\Response } } + public function sendStream($handle, $options = array()) + { + fseek($handle, 0, SEEK_END); + $fileSize = ftell($handle); + + $contentStart = 0; + $contentEnd = $fileSize - 1; + + $headers = $this->getHeaders(); + + if (isset($_SERVER['HTTP_RANGE'])) { + // client sent us a multibyte range, can not hold this one for now + if (strpos($_SERVER['HTTP_RANGE'], ',') !== false) { + $headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize"); + throw new HttpException(416, Yii::t('yii', 'Requested range not satisfiable')); + } + + $range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']); + + // range requests starts from "-", so it means that data must be dumped the end point. + if ($range[0] === '-') { + $contentStart = $fileSize - substr($range, 1); + } else { + $range = explode('-', $range); + $contentStart = $range[0]; + + // check if the last-byte-pos presents in header + if ((isset($range[1]) && is_numeric($range[1]))) { + $contentEnd = $range[1]; + } + } + + /* Check the range and make sure it's treated according to the specs. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + // End bytes can not be larger than $end. + $contentEnd = $contentEnd > $fileSize ? $fileSize - 1 : $contentEnd; + + // Validate the requested range and return an error if it's not correct. + if ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0) { + $headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize"); + throw new HttpException(416, Yii::t('yii', 'Requested range not satisfiable')); + } + + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize"); + } else { + $this->setStatusCode(200); + } + + if (isset($options['mimeType'])) { + $headers->set('Content-Type', $options['mimeType']); + } + + $length = $contentEnd - $contentStart + 1; + $disposition = empty($options['disposition']) ? 'attachment' : $options['disposition']; + if (!isset($options['saveName'])) { + $options['saveName'] = 'data'; + } + + $headers->set('Pragma', 'public') + ->set('Expires', '0') + ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->set('Content-Disposition', "$disposition; filename=\"{$options['saveName']}\"") + ->set('Content-Length', $length) + ->set('Content-Transfer-Encoding', 'binary'); + + if (isset($options['headers'])) { + foreach ($options['headers'] as $header => $value) { + $headers->add($header, $value); + } + } + + fseek($handle, $contentStart); + + set_time_limit(0); // Reset time limit for big files + + $chunkSize = 8 * 1024 * 1024; // 8MB per chunk + while (!feof($handle) && ($fPointer = ftell($handle)) <= $contentEnd) { + if ($fPointer + $chunkSize > $contentEnd) { + $chunkSize = $contentEnd - $fPointer + 1; + } + echo fread($handle, $chunkSize); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } + + fclose($handle); + } + /** * Sends existing file to a browser as a download using x-sendfile. * @@ -404,52 +493,39 @@ class Response extends \yii\base\Response * @param string $filePath file name with full path * @param array $options additional options: * - * - saveName: file name shown to the user, if not set real file name will be used - * - mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, - * if set to null no content-type header will be sent. - * - xHeader: appropriate x-sendfile header, defaults to "X-Sendfile" - * - terminate: whether to terminate the current application after calling this method, defaults to true - * - forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true - * - addHeaders: an array of additional http headers in header-value pairs + * - saveName: file name shown to the user. If not set, the name will be determined from `$filePath`. + * - mimeType: MIME type of the file. If not set, it will be determined based on the file name. + * - xHeader: appropriate x-sendfile header, defaults to "X-Sendfile". + * - disposition: either "attachment" or "inline". This specifies whether the file will be downloaded + * or shown inline. Defaults to "attachment". + * - headers: an array of additional http headers in name-value pairs. */ public function xSendFile($filePath, $options = array()) { - if (!isset($options['forceDownload']) || $options['forceDownload']) { - $disposition = 'attachment'; - } else { - $disposition = 'inline'; - } + $headers = $this->getHeaders(); - if (!isset($options['saveName'])) { - $options['saveName'] = basename($filePath); - } + $headers->set(empty($options['xHeader']) ? 'X-Sendfile' : $options['xHeader'], $filePath); if (!isset($options['mimeType'])) { if (($options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath)) === null) { $options['mimeType'] = 'text/plain'; } } + $headers->set('Content-Type', $options['mimeType']); - if (!isset($options['xHeader'])) { - $options['xHeader'] = 'X-Sendfile'; - } - - $headers = $this->getHeaders(); - - if ($options['mimeType'] !== null) { - $headers->set('Content-Type', $options['mimeType']); + $disposition = empty($options['disposition']) ? 'attachment' : $options['disposition']; + if (!isset($options['saveName'])) { + $options['saveName'] = basename($filePath); } $headers->set('Content-Disposition', "$disposition; filename=\"{$options['saveName']}\""); - if (isset($options['addHeaders'])) { - foreach ($options['addHeaders'] as $header => $value) { - $headers->set($header, $value); + + if (isset($options['headers'])) { + foreach ($options['headers'] as $header => $value) { + $headers->add($header, $value); } } - $headers->set(trim($options['xHeader']), $filePath); - if (!isset($options['terminate']) || $options['terminate']) { - Yii::$app->end(); - } + $this->send(); } /**