diff --git a/Controller/UploadController.php b/Controller/UploadController.php index 8aa8c7b260a58f8b91c182dabf62ee92e8ba5fd3..4a14530317e85c885e24e94ca64cbd2ed4069e32 100644 --- a/Controller/UploadController.php +++ b/Controller/UploadController.php @@ -102,43 +102,34 @@ class UploadController extends Controller { $this->validateToken($request); - $range = $request->headers->get('Content-Range'); - $final = true; - - if($range) { + if(null !== $range = $request->headers->get('Content-Range', null)) { $matches = []; if(!preg_match('@^bytes (\d+)-(\d+)/(\d+)$@', $range, $matches)) { throw new BadRequestHttpException("Invalid Content-Range"); } $start = intval($matches[1]); + $end = intval($matches[2]); $total = intval($matches[3]); + if($start < 0 || $start >= $end || $end >= $total) { + throw new HttpException(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE); + } - $stream = $file->open('ab'); - $stream->seek($start); - - $final = $end === ($total-1); - + $offset = $start; + $maxlen = 1 + ($end - $start); + $complete = $end === ($total-1); } else { - $stream = $file->open('wb'); + $offset = 0; + $maxlen = -1; + $complete = true; } // Demande un filehandle plutôt que charger le contenu en mémoire $input = $request->getContent(true); - - if(false !== $fileHandler = $stream->cast(STREAM_CAST_AS_STREAM)) { - // Utilise stream_copy_to_stream si le Gaufrette\Stream peut nous retourner un filehandle - stream_copy_to_stream($input, $fileHandler); - } else { - // Sinon fait une copie par blocs (moins performant) - while(!feof($input)) { - $stream->write(fread($input, 8192)); - } - } + $copied = $file->copyFrom($input, $maxlen, $offset); fclose($input); - $stream->close(); - if(!$final) { + if(!$complete) { return $this->createResponse(Response::HTTP_OK, 'Chunk received', ['range' => compact('start', 'end', 'total')]); } diff --git a/Entity/UploadedFile.php b/Entity/UploadedFile.php index d853fdfc203801c9b2a52263d5f32f400487b126..4d01b95e76258a7735bd6617ed80a3ca441c85e0 100644 --- a/Entity/UploadedFile.php +++ b/Entity/UploadedFile.php @@ -10,7 +10,6 @@ namespace Irstea\FileUploadBundle\Entity; use DateTime; use Doctrine\ORM\Mapping as ORM; use Gaufrette\Filesystem; -use Gaufrette\Stream; use Gaufrette\StreamMode; use InvalidArgumentException; use Rhumsaa\Uuid\Uuid; @@ -21,6 +20,9 @@ use Rhumsaa\Uuid\Uuid; */ class UploadedFile { + // Taille de bloc utilisé pour les copies + static public $copyBlockSize = 8192; + const ETAT_EN_COURS = 'en-cours'; const ETAT_ORPHELIN = 'orphelin'; const ETAT_NORMAL = 'normal'; @@ -348,32 +350,140 @@ class UploadedFile return $this->getEtat() === self::ETAT_ORPHELIN; } - /** + /** Retourne la date de dernière modification dans le filesystem. * - * @param type $mode - * @return Stream + * @return \DateTime */ - public function open($mode = 'rb') + public function getLastModified() { - $stream = $this->filesystem->createStream($this->getPath()); - $stream->open(new StreamMode($mode)); - return $stream; + return new \DateTime(sprintf('@%d', $this->filesystem->mtime($this->getPath()))); } - /** + /** Retourne le contenu du fichier. * - * @return string + * @return string Une chaîne si $asResource vaut faux, */ public function getContent() { return $this->filesystem->read($this->getPath()); } - /** - * @return \DateTime + /** Ecrit dans le fichier. + * + * @param string $content */ - public function getLastModified() + public function setContent($content) { - return new \DateTime(sprintf('@%d', $this->filesystem->mtime($this->getPath()))); + return $this->filesystem->write($this->getPath(), $content, true); + } + + /** Ecrit dans le fichier depuis un descripteur de fichier. + * + * @param resource $source + * @param int $maxlen + * @param int $writeOffset + * @return int + */ + public function copyFrom($source, $maxlen = PHP_INT_MAX, $writeOffset = 0) + { + if($maxlen === 0) { + return 0; + } + + $stream = $this->filesystem->createStream($this->getPath(), new StreamMode('cb')); + $stream->seek($writeOffset); + + if(false !== $fileHandler = $stream->cast(STREAM_CAST_AS_STREAM)) { + // Utilise stream_copy_to_stream si le Gaufrette\Stream peut nous retourner un filehandle + $copied = $this->stream_copy_to_stream($source, $fileHandler, $maxlen); + } else { + // Sinon fait une copie par blocs (moins performant) + $copied = 0; + while(!$this->feof($source) && $copied <= $maxlen) { + $copied += $stream->write($this->fread($source, min(static::$copyBlockSize, $maxlen - $copied))); + } + } + $stream->close(); + + return $copied; + } + + /** Envoie le contenu du fichier dan un descripteur de fichier. + * + * @param $dest $resource + * @param int $maxlen + * @param int $readOffset + * @return int + */ + public function copyTo($dest, $maxlen = PHP_INT_MAX, $readOffset = 0) + { + $actualLength = min($maxlen, $this->getSize() - $readOffset); + + if (0 <= $actualLength) { + return 0; + } + + $stream = $this->filesystem->createStream($this->getPath(), new StreamMode('rb')); + $stream->seek($readOffset); + + if(false !== $fileHandle = $stream->cast(STREAM_CAST_AS_STREAM)) { + // Utilise stream_copy_to_stream si le Stream nous renvoie un filehandle + $copied = $this->stream_copy_to_stream($fileHandle, $dest, $actualLength); + } else { + // Sinon, on fait ça à la main par blocs de 8ko + $copied = 0; + while(!$stream->eof() && $copied < $actualLength) { + $copied += $this->fwrite($dest, $stream->read(min(static::$copyBlockSize, $actualLength - $copied))); + } + } + + $stream->close(); + + return $copied; + } + + /** Wrapper de stream_copy_to_stream + * + * @param resource $source + * @param resource $dest + * @param int $maxlen + * @param int $offset + * @return int + */ + protected function stream_copy_to_stream($source, $dest, $maxlen = -1, $offset = 0) + { + return stream_copy_to_stream($source, $dest, $maxlen, $offset); + } + + /** Wrapper de feof + * + * @param resource $fh + * @return boolean + */ + protected function feof($fh) + { + return feof($fh); + } + + /** Wrapper de fread + * + * @param resource $fh + * @param int $maxlen + * @return int|boolean + */ + protected function fread($fh, $maxlen = -1) + { + return fread($fh, $maxlen); + } + + /** Wrapper de fwrite + * + * @param resource $fh + * @param int $maxlen + * @return int|boolean + */ + protected function fwrite($fh, $maxlen = -1) + { + return fwrite($fh, $maxlen); } } diff --git a/Http/UploadedFileResponse.php b/Http/UploadedFileResponse.php index 034201517828ba5d363459d6892a4e849fa849d0..41402d212a7dad90b9ed6036678efdd49e4a1e91 100644 --- a/Http/UploadedFileResponse.php +++ b/Http/UploadedFileResponse.php @@ -203,24 +203,7 @@ class UploadedFileResponse extends Response } $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(); + $this->file->copyTo($out, $this->maxlen, $this->offset); fclose($out); }