<?php declare(strict_types=1); /* * Copyright (C) 2015-2017 IRSTEA * All rights reserved. */ namespace Irstea\FileUploadBundle\Entity; use DateTime; use Doctrine\ORM\Mapping as ORM; use Gaufrette\Exception\FileNotFound; use Gaufrette\Filesystem; use Gaufrette\StreamMode; use InvalidArgumentException; use Irstea\FileUploadBundle\Model\UploadedFileInterface; use Irstea\FileUploadBundle\Utils\MimeTypeIcon; use Rhumsaa\Uuid\Uuid; /** * @ORM\Entity(repositoryClass="Irstea\FileUploadBundle\Entity\Repository\UploadedFileRepository") * @ORM\EntityListeners({ * "Irstea\FileUploadBundle\Listener\UploadedFileListener", * "Irstea\FileUploadBundle\Listener\CreationDataListener" * }) * @ORM\HasLifecycleCallbacks */ class UploadedFile implements UploadedFileInterface { /** * Taille de bloc utilisé pour les copies. * * @var int */ public static $copyBlockSize = 8192; public const ORPHAN_PREFIX = 'orphan/'; /** * @ORM\Id * @ORM\Column(type="guid") * * @var string */ private $id; /** * @ORM\Column(type="string", length=1024) * * @var string */ private $displayName; /** * @ORM\Column(type="string", length=1024) * * @var string */ private $path; /** * @var string */ private $actualPath; /** * @ORM\Column(type="string", length=255, nullable=true) * * @var string */ private $mimeType; /** * @ORM\Column(type="integer", nullable=true) * * @var int */ private $size; /** * @ORM\Column(type="string", length=64, nullable=true) * * @var string */ private $checksum; /** * @ORM\Column(type="string", length=10) * * @var string */ private $etat = self::ETAT_EN_COURS; /** * @ORM\Column(type="datetime") * * @var DateTime */ private $createdAt; /** * @ORM\Column(type="string", nullable=true) * * @var string */ private $createdBy; /** * @ORM\Column(type="string", nullable=true) * * @var string */ private $createdFrom; /** * @ORM\Column(type="json_array", nullable=true) * * @var array */ private $metadata; /** * @ORM\Column(type="string", length=256, nullable=true) * * @var string */ private $description; /** * @var Filesystem */ private $filesystem; /** Contient le nom de chemin local. * @var string */ private $localTempPath; /** Crée un UploadedFile. * @internal */ public function __construct() { $this->id = Uuid::uuid4()->toString(); $this->actualPath = $this->path = self::ORPHAN_PREFIX . $this->id; } /** * {@inheritdoc} */ public function getId() { return $this->id; } /** * {@inheritdoc} */ public function setDisplayName($displayName) { $this->displayName = $displayName; return $this; } /** * {@inheritdoc} */ public function getDisplayName() { return $this->displayName; } /** * {@inheritdoc} */ public function getPath() { return $this->path; } /** * @return string */ public function getActualPath() { if (null === $this->actualPath) { $this->actualPath = $this->path; } return $this->actualPath; } /** * {@inheritdoc} */ public function setPath($path) { if (!static::isSafePath($path)) { throw new InvalidArgumentException("Unsafe path: $path"); } $this->path = trim($path, '/'); return $this; } /** * {@inheritdoc} */ public function moveTo($newDir) { $this->setPath(rtrim($newDir, '/') . '/' . pathinfo($this->path, PATHINFO_FILENAME)); } /** * {@inheritdoc} */ public function setMimeType($mimeType) { $this->mimeType = $mimeType; return $this; } /** * {@inheritdoc} */ public function getMimeType() { return $this->mimeType; } /** * {@inheritdoc} */ public function setSize($size) { $this->size = $size; return $this; } /** * {@inheritdoc} */ public function getSize() { return $this->size; } /** * {@inheritdoc} */ public function setChecksum($checksum) { $this->checksum = $checksum; return $this; } /** * {@inheritdoc} */ public function getChecksum() { return $this->checksum; } /** * {@inheritdoc} */ public function setEtat($etat) { if (!in_array( $etat, [ self::ETAT_CORROMPU, self::ETAT_EN_COURS, self::ETAT_MANQUANT, self::ETAT_NORMAL, self::ETAT_ORPHELIN, self::ETAT_REJETE, ], true )) { throw new InvalidArgumentException(sprintf("Etat invalide: '%s'", (string) $etat)); } // Déplace le fichier hors de l'orphelinat quand on passe d'orphelin à nouveau if ($this->etat === self::ETAT_ORPHELIN && $etat === self::ETAT_NORMAL && 0 === strpos($this->path, self::ORPHAN_PREFIX)) { $this->path = substr($this->path, strlen(self::ORPHAN_PREFIX)); } $this->etat = $etat; return $this; } /** * {@inheritdoc} */ public function getEtat() { return $this->etat; } /** * {@inheritdoc} */ public function getCreatedAt() { return $this->createdAt; } /** * {@inheritdoc} */ public function setMetadata(array $metadata = null) { $this->metadata = $metadata; return $this; } /** * {@inheritdoc} */ public function getMetadata() { return $this->metadata; } /** * {@inheritdoc} */ public function __toString() { $unit = ''; $size = $this->size ?: 0; if ($size >= 10240) { $size /= 1024; $unit = 'k'; if ($size >= 10240) { $size /= 1024; $unit = 'm'; } } return sprintf('%s (%s, %d%so)', $this->displayName, $this->mimeType ?: '?/?', $size, $unit); } /** * @param Filesystem $filesystem * * @return self * * @internal */ public function setFilesystem(Filesystem $filesystem) { $this->filesystem = $filesystem; return $this; } /** * {@inheritdoc} */ public function validate() { if (self::ETAT_EN_COURS === $this->getEtat()) { return; } $filesystem = $this->filesystem; $path = $this->getActualPath(); if (!$filesystem->has($path)) { $this->setEtat(self::ETAT_MANQUANT); return; } if ($filesystem->size($path) !== $this->size || $filesystem->checksum($path) !== $this->checksum) { $this->setEtat(self::ETAT_CORROMPU); return; } } /** * {@inheritdoc} */ public function isValid() { return $this->getEtat() === self::ETAT_ORPHELIN || $this->getEtat() === self::ETAT_NORMAL; } /** * {@inheritdoc} */ public function isOrphelin() { return $this->getEtat() === self::ETAT_ORPHELIN; } /** * {@inheritdoc} */ public function getLastModified() { try { return new \DateTime(sprintf('@%d', $this->filesystem->mtime($this->getActualPath()))); } catch (FileNotFound $ex) { return null; } } /** * {@inheritdoc} */ public function getContent() { return $this->filesystem->read($this->getActualPath()); } /** * {@inheritdoc} */ public function setContent($content) { return $this->filesystem->write($this->getActualPath(), $content, true); } /** * {@inheritdoc} */ public function copyFrom($source, $maxlen = -1, $writeOffset = 0) { if ($maxlen === 0) { return 0; } $stream = $this->filesystem->createStream($this->getActualPath()); $stream->open(new StreamMode('cb')); $stream->seek($writeOffset); if (false !== $fileHandle = $stream->cast(STREAM_CAST_AS_STREAM)) { // Utilise stream_copy_to_stream si le Gaufrette\Stream peut nous retourner un filehandle $copied = stream_copy_to_stream($source, $fileHandle, $maxlen); } else { // Sinon fait une copie par blocs (moins performant) if ($maxlen === -1) { $maxlen = PHP_INT_MAX; } $copied = 0; while (!feof($source) && $copied <= $maxlen) { $copied += $stream->write(fread($source, min(static::$copyBlockSize, $maxlen - $copied))); } } $stream->close(); return $copied; } /** * {@inheritdoc} */ public function copyTo($dest, $maxlen = -1, $readOffset = 0) { if ($maxlen === -1) { $actualLength = $this->getSize() - $readOffset; } else { $actualLength = min($maxlen, $this->getSize() - $readOffset); } if ($actualLength <= 0) { return 0; } $stream = $this->filesystem->createStream($this->getActualPath()); $stream->open(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 = 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 += fwrite($dest, $stream->read(min(static::$copyBlockSize, $actualLength - $copied))); } } $stream->close(); return $copied; } /** Vérifie si un chemin est "safe". * @param string $path * * @return bool * * @internal */ public static function isSafePath($path) { /** * @return string */ $parts = explode('/', trim($path, '/')); $level = 0; foreach ($parts as $part) { switch ($part) { case '.': break; case '..': $level--; if ($level < 0) { return false; } break; default: $level++; } } return true; } /** * {@inheritdoc} */ public function getDescription() { return $this->description; } /** * {@inheritdoc} */ public function setDescription($description = null) { $this->description = $description; return $this; } /** * {@inheritdoc} */ public function toArray() { return [ 'id' => $this->getId(), 'name' => $this->getDisplayName(), 'size' => $this->getSize(), 'type' => $this->getMimeType(), 'etat' => $this->getEtat(), 'description' => $this->getDescription(), 'checksum' => $this->getChecksum(), 'icon' => MimeTypeIcon::getMimeTypeIcon($this->getMimeType()), ]; } /** * {@inheritdoc} */ public function getCreatedBy() { return $this->createdBy; } /** * {@inheritdoc} */ public function getCreatedFrom() { return $this->createdFrom; } /** * {@inheritdoc} */ public function setCreatedAt(DateTime $createdAt) { $this->createdAt = $createdAt; return $this; } /** * {@inheritdoc} */ public function setCreatedBy($createdBy) { $this->createdBy = $createdBy; return $this; } /** * {@inheritdoc} */ public function setCreatedFrom($createdFrom) { $this->createdFrom = $createdFrom; return $this; } /** * {@inheritdoc} */ public function getLocalPath() { if (null !== $this->localTempPath) { return $this->localTempPath; } $stream = $this->filesystem->createStream($this->getActualPath()); $stream->open(new StreamMode('rb')); $handle = $stream->cast(STREAM_CAST_AS_STREAM); if (false !== $handle) { if (stream_is_local($handle)) { $this->localTempPath = stream_get_meta_data($handle)['uri']; fclose($handle); return $this->localTempPath; } fclose($handle); } $this->localTempPath = tempnam(sys_get_temp_dir(), 'UploadedFile'); register_shutdown_function('unlink', $this->localTempPath); $tmp = fopen($this->localTempPath, 'xb'); $this->copyTo($tmp); fclose($tmp); return $this->localTempPath; } /** * Met à jour le chemin réel du fichier. * * @ORM\PostLoad * @ORM\PostPersist * @ORM\PostUpdate */ public function updateActualPath() { $this->actualPath = $this->path; } /** * {@inheritdoc} * Ne compare que la taille et la somme de contrôle : il existe une très faible probabilité que deux fichiers de * même taille et même checksum soient différents. */ public function hasSameContent(UploadedFileInterface $other) { return $other->getSize() === $this->getSize() && $other->getChecksum() === $this->getChecksum(); } }