diff --git a/Controller/UploadController.php b/Controller/UploadController.php index a41a090c5840f417f7c888223bd74f3f55ebc711..8aa8c7b260a58f8b91c182dabf62ee92e8ba5fd3 100644 --- a/Controller/UploadController.php +++ b/Controller/UploadController.php @@ -8,6 +8,7 @@ namespace Irstea\FileUploadBundle\Controller; use Irstea\FileUploadBundle\Entity\UploadedFile; +use Irstea\FileUploadBundle\Http\UploadedFileResponse; use Irstea\FileUploadBundle\Service\FileManagerInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; @@ -17,7 +18,6 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -182,24 +182,8 @@ class UploadController extends Controller throw new NotFoundHttpException(); } - $response = StreamedResponse::create(function() use($file) { echo $file->getContent(); }) - ->setPrivate() - ->setLastModified($file->getLastModified()) - ->setEtag($file->getChecksum()); - - $response->headers->add( - [ - 'Content-Type' => $file->getMimeType(), - 'Content-Length' => $file->getSize(), - 'Content-Disposition' => $response->headers->makeDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $file->getOriginalFilename() - ), - ] - ); - + $response = UploadedFileResponse::create($file, 200, [], false, ResponseHeaderBag::DISPOSITION_ATTACHMENT); $response->isNotModified($request); - return $response; } diff --git a/Http/UploadedFileResponse.php b/Http/UploadedFileResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..034201517828ba5d363459d6892a4e849fa849d0 --- /dev/null +++ b/Http/UploadedFileResponse.php @@ -0,0 +1,248 @@ +<?php + +/* + * Copyright (C) 2015 IRSTEA + * All rights reserved. + */ + +namespace Irstea\FileUploadBundle\Http; + +use DateTime; +use Irstea\FileUploadBundle\Entity\UploadedFile; +use LogicException; +use SplFileInfo; +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Description of UploadedFileResponse + * + * Très, très, fortement inspiré de Symfony\Component\HttpFoundation\UploadedFileResponse. + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class UploadedFileResponse extends Response +{ + /** + * + * @var UploadedFile + */ + protected $file; + + protected $offset = 0; + + protected $maxlen; + + /** + * Constructor. + * + * @param UploadedFile $file The file to stream + * @param int $status The response status code + * @param array $headers An array of response headers + * @param bool $public Files are public by default + * @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename + */ + public function __construct(UploadedFile $file = null, $status = 200, $headers = array(), $public = true, $contentDisposition = null) + { + parent::__construct(null, $status, $headers); + + if ($file) { + $this->setFile($file, $contentDisposition); + } + + if ($public) { + $this->setPublic(); + } + } + + /** + * @param UploadedFile $file The file to stream + * @param int $status The response status code + * @param array $headers An array of response headers + * @param bool $public Files are public by default + * @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename + * + * @return UploadedFileResponse The created response + */ + public static function create($file = null, $status = 200, $headers = array(), $public = true, $contentDisposition = null) + { + return new static($file, $status, $headers, $public, $contentDisposition); + } + + + /** + * Sets the file to stream. + * + * @param UploadedFile $file The file to stream + * @param string $contentDisposition + * + * @return UploadedFileResponse + * + * @throws FileException + */ + public function setFile(UploadedFile $file, $contentDisposition = null) + { + $this->file = $file; + + $this + ->setLastModified($this->file->getLastModified()) + ->setEtag($file->getChecksum()); + + if ($contentDisposition) { + $this->setContentDisposition($contentDisposition); + } + + return $this; + } + + /** + * Gets the file. + * + * @return UploadedFile The file to stream + */ + public function getFile() + { + return $this->file; + } + + /** + * Sets the Content-Disposition header with the given filename. + * + * @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT + * @param string $filename Optionally use this filename instead of the real name of the file + * @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename + * + * @return UploadedFileResponse + */ + public function setContentDisposition($disposition, $filename = '', $filenameFallback = '') + { + if ($filename === '') { + $filename = $this->file->getOriginalFilename(); + } + + $dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback); + $this->headers->set('Content-Disposition', $dispositionHeader); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function prepare(Request $request) + { + $this->headers->set('Content-Length', $this->file->getSize()); + + if (!$this->headers->has('Accept-Ranges')) { + // Only accept ranges on safe HTTP methods + $this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none'); + } + + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); + } + + if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) { + $this->setProtocolVersion('1.1'); + } + + $this->ensureIEOverSSLCompatibility($request); + + $this->offset = 0; + $this->maxlen = -1; + + if ($request->headers->has('Range')) { + // Process the range headers. + if (!$request->headers->has('If-Range') || $this->getEtag() == $request->headers->get('If-Range')) { + $range = $request->headers->get('Range'); + $fileSize = $this->file->getSize(); + + list($start, $end) = explode('-', substr($range, 6), 2) + array(0); + + $end = ('' === $end) ? $fileSize - 1 : (int) $end; + + if ('' === $start) { + $start = $fileSize - $end; + $end = $fileSize - 1; + } else { + $start = (int) $start; + } + + if ($start <= $end) { + if ($start < 0 || $end > $fileSize - 1) { + $this->setStatusCode(416); + } elseif ($start !== 0 || $end !== $fileSize - 1) { + $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; + $this->offset = $start; + + $this->setStatusCode(206); + $this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); + $this->headers->set('Content-Length', $end - $start + 1); + } + } + } + } + + return $this; + } + + /** + * Sends the file. + */ + public function sendContent() + { + if (!$this->isSuccessful()) { + parent::sendContent(); + + return; + } + + if (0 === $this->maxlen) { + return; + } + + $out = fopen('php://output', 'wb'); + $stream = $this->file->open('rb'); + + if(false !== $fileHandle = $stream->cast(STREAM_CAST_AS_STREAM)) { + // Utilise stream_copy_to_stream si le Stream nous renvoie un filehandle + stream_copy_to_stream($fileHandle, $out, $this->maxlen, $this->offset); + } else { + // Sinon, on fait ça à la main par blocs de 8ko + $stream->seek($this->offset); + for($i = floor($this->maxlen / 8192); $i > 0; $i--) { + fwrite($out, $stream->read(8192)); + } + $remaining = $this->maxlen % 8192; + if($remaining > 0) { + fwrite($out, $stream->read($remaining)); + } + } + + $stream->close(); + fclose($out); + } + + /** + * {@inheritdoc} + * + * @throws LogicException when the content is not null + */ + public function setContent($content) + { + if (null !== $content) { + throw new LogicException('The content cannot be set on a UploadedFileResponse instance.'); + } + } + + /** + * {@inheritdoc} + * + * @return false + */ + public function getContent() + { + return false; + } +}